@openhands/extensions 0.15.0 → 0.17.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/.release-please-manifest.json +1 -1
- package/AGENTS.md +1 -0
- package/automations/README.md +97 -18
- package/automations/bundle-index.js +9 -0
- package/automations/catalog/github-pr-reviewer/manifest.json +23 -7
- package/automations/catalog/github-repo-monitor/manifest.json +28 -34
- package/automations/catalog/jira-issue-to-pr/manifest.json +3 -3
- package/automations/catalog.schema.json +102 -1
- package/automations/index.d.ts +174 -1
- package/automations/index.js +14 -0
- package/automations/interface.json +93 -6
- package/automations/interface.schema.json +273 -9
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/integrations/catalog/posthog.json +41 -3
- package/integrations/catalog/sonarqube.json +78 -0
- package/integrations/catalog-index.js +65 -63
- package/marketplaces/openhands-extensions.json +11 -0
- package/package.json +1 -1
- package/plugins/qa-changes/action.yml +1 -1
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/scripts/build-automation-catalog.mjs +30 -0
- package/scripts/build-skills-catalog.mjs +24 -7
- package/skills/add-skill/SKILL.md +1 -1
- package/skills/github-pr-reviewer/README.md +12 -6
- package/skills/github-pr-reviewer/SKILL.md +93 -31
- package/skills/github-pr-reviewer/commands/pr-reviewer-setup.md +1 -1
- package/skills/github-pr-reviewer/references/state-schema.md +68 -18
- package/skills/github-pr-reviewer/scripts/main.py +540 -103
- package/skills/github-pr-reviewer/tests/test_main.py +503 -0
- package/skills/index.d.ts +6 -0
- package/skills/index.js +29 -17
- package/skills/jira-issue-to-pr/scripts/main.py +4 -2
- package/skills/openhands-sdk/SKILL.md +2 -0
- package/skills/skill-creator/SKILL.md +1 -1
- package/skills/slack-channel-monitor/scripts/main.py +3 -2
- package/tests/fixtures/automations/capabilities.json +30 -0
- package/tests/fixtures/automations/github-pr-reviewer.json +319 -55
- package/tests/fixtures/automations/github-repo-monitor.json +153 -62
- package/tests/test_automation_setup.py +226 -31
- package/tests/test_catalogs.py +43 -0
- package/tests/test_interface_manifest.py +20 -0
- package/tests/test_skills_catalog.py +182 -36
- package/tests/test_slack_channel_monitor.py +1 -1
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"source": "./skills/add-skill",
|
|
30
30
|
"description": "Add (import) an OpenHands skill from a GitHub repository into the current workspace.",
|
|
31
31
|
"category": "agent-authoring",
|
|
32
|
+
"defaultEnabled": true,
|
|
32
33
|
"keywords": [
|
|
33
34
|
"skill",
|
|
34
35
|
"import",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"source": "./skills/agent-memory",
|
|
42
43
|
"description": "Persist and retrieve repository-specific knowledge using AGENTS.md files. Use when you want to save important information about a codebase (build commands, code style, workflows) for future sessions.",
|
|
43
44
|
"category": "agent-authoring",
|
|
45
|
+
"defaultEnabled": true,
|
|
44
46
|
"keywords": [
|
|
45
47
|
"memory",
|
|
46
48
|
"knowledge",
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
"source": "./skills/agent-sdk-builder",
|
|
54
56
|
"description": "Guided workflow for building custom AI agents using the OpenHands Software Agent SDK. Use when you want to create a new agent through an interactive interview process that gathers requirements and generates implementation plans.",
|
|
55
57
|
"category": "agent-authoring",
|
|
58
|
+
"defaultEnabled": true,
|
|
56
59
|
"keywords": [
|
|
57
60
|
"agent",
|
|
58
61
|
"sdk",
|
|
@@ -80,6 +83,7 @@
|
|
|
80
83
|
"source": "./skills/openhands-automation",
|
|
81
84
|
"description": "Create and manage OpenHands automations - scheduled tasks that run in sandboxes. Use the prompt preset to create automations from natural language, or manage existing automations.",
|
|
82
85
|
"category": "automations",
|
|
86
|
+
"defaultEnabled": true,
|
|
83
87
|
"keywords": [
|
|
84
88
|
"automation",
|
|
85
89
|
"cron",
|
|
@@ -93,6 +97,7 @@
|
|
|
93
97
|
"source": "./skills/agent-canvas-environment",
|
|
94
98
|
"description": "Work effectively inside a local Agent Canvas environment, including local agent-server auth, safe workspace hygiene, and local conversation delegation.",
|
|
95
99
|
"category": "agent-authoring",
|
|
100
|
+
"defaultEnabled": true,
|
|
96
101
|
"keywords": [
|
|
97
102
|
"agent-canvas",
|
|
98
103
|
"openhands",
|
|
@@ -106,6 +111,7 @@
|
|
|
106
111
|
"source": "./skills/openhands-sdk",
|
|
107
112
|
"description": "Reference skill for the OpenHands Software Agent SDK - build AI agents with custom tools, LLM configuration, conversations, sub-agent delegation, MCP integration, security, and persistence.",
|
|
108
113
|
"category": "agent-authoring",
|
|
114
|
+
"defaultEnabled": true,
|
|
109
115
|
"keywords": [
|
|
110
116
|
"sdk",
|
|
111
117
|
"agent",
|
|
@@ -182,6 +188,7 @@
|
|
|
182
188
|
"source": "./skills/code-review",
|
|
183
189
|
"description": "Rigorous code review focusing on data structures, simplicity, security, pragmatism, and risk/safety evaluation. Provides brutally honest, actionable feedback on pull requests or merge requests, including a risk assessment for every review. Use when reviewing code changes.",
|
|
184
190
|
"category": "code-quality",
|
|
191
|
+
"defaultEnabled": true,
|
|
185
192
|
"keywords": [
|
|
186
193
|
"code-review",
|
|
187
194
|
"quality",
|
|
@@ -232,6 +239,7 @@
|
|
|
232
239
|
"source": "./skills/docker",
|
|
233
240
|
"description": "Run Docker commands within a container environment, including starting the Docker daemon and managing containers. Use when building, running, or managing Docker containers and images.",
|
|
234
241
|
"category": "environment",
|
|
242
|
+
"defaultEnabled": true,
|
|
235
243
|
"keywords": [
|
|
236
244
|
"docker",
|
|
237
245
|
"container",
|
|
@@ -268,6 +276,7 @@
|
|
|
268
276
|
"source": "./skills/github",
|
|
269
277
|
"description": "Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable and GitHub CLI. Use when working with code hosted on GitHub or managing GitHub resources.",
|
|
270
278
|
"category": "code-hosting",
|
|
279
|
+
"defaultEnabled": true,
|
|
271
280
|
"keywords": [
|
|
272
281
|
"github",
|
|
273
282
|
"git",
|
|
@@ -444,6 +453,7 @@
|
|
|
444
453
|
"source": "./skills/openhands-api",
|
|
445
454
|
"description": "Use the OpenHands Cloud REST API (V1) and agent-server APIs to create and manage Cloud or local backend conversations, including multi-conversation delegation workflows. Includes minimal Python and TypeScript clients under scripts/.",
|
|
446
455
|
"category": "agent-authoring",
|
|
456
|
+
"defaultEnabled": true,
|
|
447
457
|
"keywords": [
|
|
448
458
|
"openhands",
|
|
449
459
|
"api",
|
|
@@ -534,6 +544,7 @@
|
|
|
534
544
|
"source": "./skills/skill-creator",
|
|
535
545
|
"description": "Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.",
|
|
536
546
|
"category": "agent-authoring",
|
|
547
|
+
"defaultEnabled": true,
|
|
537
548
|
"keywords": [
|
|
538
549
|
"skill",
|
|
539
550
|
"plugin",
|
package/package.json
CHANGED
|
@@ -160,7 +160,7 @@ runs:
|
|
|
160
160
|
# so we enforce the time limit via the coreutils timeout command.
|
|
161
161
|
TIMEOUT_SECONDS=$((TIMEOUT_MINUTES * 60))
|
|
162
162
|
timeout "${TIMEOUT_SECONDS}" \
|
|
163
|
-
uv run --no-project --with openhands-sdk --with openhands-tools --with
|
|
163
|
+
uv run --no-project --with openhands-sdk --with openhands-tools --with lmnr \
|
|
164
164
|
python ../extensions/plugins/qa-changes/scripts/agent_script.py
|
|
165
165
|
|
|
166
166
|
- name: Upload logs as artifact
|
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.17.0" # x-release-please-version
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
24
|
__version__: str = version("openhands-extensions")
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
const root = process.cwd();
|
|
5
5
|
const catalogDir = path.join(root, "automations", "catalog");
|
|
6
6
|
const outputPath = path.join(root, "automations", "catalog-index.js");
|
|
7
|
+
const bundleOutputPath = path.join(root, "automations", "bundle-index.js");
|
|
7
8
|
|
|
8
9
|
const directories = (await readdir(catalogDir, { withFileTypes: true }))
|
|
9
10
|
.filter((entry) => entry.isDirectory())
|
|
@@ -37,6 +38,25 @@ const imports = indexedAutomations
|
|
|
37
38
|
.join("\n");
|
|
38
39
|
const entries = indexedAutomations.map(({ importName }) => ` ${importName},`).join("\n");
|
|
39
40
|
|
|
41
|
+
// A bundle entry names its files by repository-relative path, so the one copy
|
|
42
|
+
// of a script shipped by both a skill and the catalog stays in one place. The
|
|
43
|
+
// package cannot ship a path, though - a browser host has no repository - so
|
|
44
|
+
// the contents are inlined here at build time.
|
|
45
|
+
const bundles = {};
|
|
46
|
+
for (const { entry } of automations) {
|
|
47
|
+
const files = entry.setup?.bundle?.files;
|
|
48
|
+
if (!files) continue;
|
|
49
|
+
bundles[entry.id] = {};
|
|
50
|
+
for (const [packedPath, source] of Object.entries(files).sort()) {
|
|
51
|
+
if (!/^(skills|automations)\//.test(source) || source.includes("..")) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${entry.id}: bundle file "${packedPath}" names ${source}, which is outside skills/ or automations/`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
bundles[entry.id][packedPath] = await readFile(path.join(root, source), "utf8");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
const header = `// This file is auto-generated by scripts/build-automation-catalog.mjs.
|
|
41
61
|
// Do not edit it manually. To update it after changing automations/catalog/*/manifest.json,
|
|
42
62
|
// run: npm run build:automations
|
|
@@ -49,3 +69,13 @@ ${entries}
|
|
|
49
69
|
];
|
|
50
70
|
`;
|
|
51
71
|
await writeFile(outputPath, body);
|
|
72
|
+
|
|
73
|
+
const bundleHeader = `// This file is auto-generated by scripts/build-automation-catalog.mjs.
|
|
74
|
+
// Do not edit it manually. It inlines the files each bundle entry ships, read from
|
|
75
|
+
// the repository paths its manifest names. To update it, run: npm run build:automations
|
|
76
|
+
|
|
77
|
+
`;
|
|
78
|
+
await writeFile(
|
|
79
|
+
bundleOutputPath,
|
|
80
|
+
`${bundleHeader}export const AUTOMATION_BUNDLE_FILES = ${JSON.stringify(bundles, null, 2)};\n`,
|
|
81
|
+
);
|
|
@@ -36,8 +36,8 @@ export const SKILL_CATEGORY_IDS = [
|
|
|
36
36
|
|
|
37
37
|
const FALLBACK_CATEGORY = "other";
|
|
38
38
|
|
|
39
|
-
/** Build a `skill directory name -> {category, file}` map from every manifest. */
|
|
40
|
-
export function
|
|
39
|
+
/** Build a `skill directory name -> {category, defaultEnabled, file}` map from every manifest. */
|
|
40
|
+
export function buildEntryMap(marketplacesDir) {
|
|
41
41
|
const map = new Map();
|
|
42
42
|
|
|
43
43
|
for (const filename of readdirSync(marketplacesDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
@@ -56,14 +56,27 @@ export function buildCategoryMap(marketplacesDir) {
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
if ("defaultEnabled" in entry && typeof entry.defaultEnabled !== "boolean") {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${filename}: skill "${name}" has defaultEnabled ${JSON.stringify(entry.defaultEnabled)}, expected a boolean`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const defaultEnabled = entry.defaultEnabled === true;
|
|
66
|
+
|
|
59
67
|
const existing = map.get(name);
|
|
60
68
|
if (existing && existing.category !== category) {
|
|
61
69
|
throw new Error(
|
|
62
70
|
`Conflicting categories for skill "${name}": ${existing.file} says "${existing.category}", ${filename} says "${category}"`,
|
|
63
71
|
);
|
|
64
72
|
}
|
|
73
|
+
if (existing && existing.defaultEnabled !== defaultEnabled) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Conflicting defaultEnabled for skill "${name}": ${existing.file} says ${existing.defaultEnabled}, ${filename} says ${defaultEnabled}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
65
78
|
|
|
66
|
-
map.set(name, { category, file: filename });
|
|
79
|
+
map.set(name, { category, defaultEnabled, file: filename });
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -114,7 +127,7 @@ export function parseFrontmatter(raw) {
|
|
|
114
127
|
*/
|
|
115
128
|
export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) {
|
|
116
129
|
const entries = [];
|
|
117
|
-
const
|
|
130
|
+
const manifestEntries = buildEntryMap(marketplacesDir);
|
|
118
131
|
const uncategorized = [];
|
|
119
132
|
|
|
120
133
|
for (const dir of readdirSync(skillsDir).sort()) {
|
|
@@ -133,7 +146,7 @@ export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) {
|
|
|
133
146
|
const fm = parseFrontmatter(parts[1]);
|
|
134
147
|
const body = parts.slice(2).join("---").trim();
|
|
135
148
|
|
|
136
|
-
const mapped =
|
|
149
|
+
const mapped = manifestEntries.get(dir);
|
|
137
150
|
if (!mapped) uncategorized.push(dir);
|
|
138
151
|
|
|
139
152
|
entries.push({
|
|
@@ -142,6 +155,8 @@ export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) {
|
|
|
142
155
|
triggers: fm.triggers,
|
|
143
156
|
content: body,
|
|
144
157
|
category: mapped?.category ?? FALLBACK_CATEGORY,
|
|
158
|
+
// Only when true, so off-by-default has one shape.
|
|
159
|
+
...(mapped?.defaultEnabled ? { defaultEnabled: true } : {}),
|
|
145
160
|
...(fm.license ? { license: fm.license } : {}),
|
|
146
161
|
...(fm.compatibility ? { compatibility: fm.compatibility } : {}),
|
|
147
162
|
});
|
|
@@ -160,14 +175,16 @@ export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) {
|
|
|
160
175
|
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
161
176
|
if (isMain) {
|
|
162
177
|
const entries = buildCatalog(SKILLS_DIR);
|
|
178
|
+
const defaultEnabledNames = entries.filter((e) => e.defaultEnabled).map((e) => e.name);
|
|
163
179
|
|
|
164
180
|
const source = `// Auto-generated by scripts/build-skills-catalog.mjs — do not edit.
|
|
165
|
-
// Source of truth: skills/*/SKILL.md and marketplaces/*.json (category)
|
|
181
|
+
// Source of truth: skills/*/SKILL.md and marketplaces/*.json (category, defaultEnabled)
|
|
166
182
|
export const SKILL_CATEGORY_IDS = ${JSON.stringify(SKILL_CATEGORY_IDS)};
|
|
167
183
|
export const SKILLS_CATALOG = ${JSON.stringify(entries, null, 2)};
|
|
184
|
+
export const DEFAULT_ENABLED_SKILL_NAMES = ${JSON.stringify(defaultEnabledNames)};
|
|
168
185
|
export default SKILLS_CATALOG;
|
|
169
186
|
`;
|
|
170
187
|
|
|
171
188
|
writeFileSync(OUTPUT, source);
|
|
172
|
-
console.log(`Generated ${OUTPUT} with ${entries.length} skills`);
|
|
189
|
+
console.log(`Generated ${OUTPUT} with ${entries.length} skills (${defaultEnabledNames.length} default-enabled)`);
|
|
173
190
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: add-skill
|
|
3
|
-
description:
|
|
3
|
+
description: Import an existing skill from a GitHub repository URL into the current workspace. Use only when the user provides or references a GitHub URL/repo to fetch from (e.g., `/add-skill https://github.com/OpenHands/extensions/tree/main/skills/codereview` or "add the codereview skill from https://github.com/OpenHands/extensions/"). Handles fetching the skill files and placing them in .agents/skills/. This does not author new skills — to create a new skill from scratch (no source URL), use the skill-creator skill instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Add Skill
|
|
@@ -12,25 +12,31 @@ This skill is activated by:
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- Reviews PRs on demand by watching for a GitHub label event
|
|
15
|
+
- Watches several repositories from a single automation, each with its own state
|
|
15
16
|
- Processes each label application exactly once, with persistent state
|
|
16
17
|
- Re-review support by removing and re-applying the label
|
|
17
18
|
- Suppresses stale reviews when the PR head commit changes mid-review
|
|
18
|
-
-
|
|
19
|
-
|
|
19
|
+
- Hands the agent the reviewed commit already checked out, and removes that
|
|
20
|
+
checkout when the review ends, so nothing accumulates between runs
|
|
21
|
+
- Publishes a real pull request review, with inline comments where a finding
|
|
22
|
+
maps to a changed line, and verifies on GitHub that it landed
|
|
23
|
+
- Posts acknowledgement comments with AI disclosure
|
|
20
24
|
- Configurable review tone and polling schedule
|
|
21
25
|
|
|
22
26
|
## Prerequisites
|
|
23
27
|
|
|
24
28
|
Set `GITHUB_PERSONAL_ACCESS_TOKEN` in OpenHands Settings -> Secrets. The token
|
|
25
|
-
must be able to read the
|
|
26
|
-
write issue comments
|
|
29
|
+
must be able to read the repositories and their contents, read issue events,
|
|
30
|
+
write issue comments, and **write pull request reviews** — the review is
|
|
31
|
+
published through the pull request reviews API, so read-only pull request access
|
|
32
|
+
is not enough.
|
|
27
33
|
|
|
28
34
|
## Quick Start
|
|
29
35
|
|
|
30
36
|
Ask OpenHands:
|
|
31
37
|
|
|
32
|
-
> "Set up a PR review automation for my `myorg/backend`
|
|
33
|
-
> `openhands-review` label and concise reviews."
|
|
38
|
+
> "Set up a PR review automation for my `myorg/backend` and `myorg/frontend`
|
|
39
|
+
> repos using the `openhands-review` label and concise reviews."
|
|
34
40
|
|
|
35
41
|
After setup, apply the configured label to a pull request to queue a review. To
|
|
36
42
|
request another review later, remove and re-apply the label.
|
|
@@ -2,23 +2,30 @@
|
|
|
2
2
|
name: github-pr-reviewer
|
|
3
3
|
description: >
|
|
4
4
|
Create an automation that reviews GitHub pull requests when a configurable
|
|
5
|
-
trigger label is applied. Polls
|
|
6
|
-
OpenHands review conversation per label event
|
|
7
|
-
|
|
5
|
+
trigger label is applied. Polls one or more repositories deterministically,
|
|
6
|
+
starts one OpenHands review conversation per label event with the pull
|
|
7
|
+
request's head commit already checked out, and publishes the review to GitHub.
|
|
8
8
|
triggers:
|
|
9
9
|
- /pr-reviewer:setup
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# GitHub PR Reviewer Automation
|
|
13
13
|
|
|
14
|
-
Create a cron automation that watches
|
|
15
|
-
with a review trigger label, starts an OpenHands review conversation
|
|
16
|
-
label event, and
|
|
14
|
+
Create a cron automation that watches one or more GitHub repositories for pull
|
|
15
|
+
requests with a review trigger label, starts an OpenHands review conversation
|
|
16
|
+
once per label event, and publishes the AI review to GitHub.
|
|
17
17
|
Windows PowerShell equivalents for the setup, packaging, upload, and API-check shell snippets are in `references/windows.md`.
|
|
18
18
|
|
|
19
19
|
The automation script is deterministic: PR discovery, label-event tracking,
|
|
20
|
-
state persistence, stale-result suppression,
|
|
21
|
-
handled in Python. The LLM is invoked only for the review
|
|
20
|
+
state persistence, stale-result suppression, the repository checkout, and its
|
|
21
|
+
removal are all handled in Python. The LLM is invoked only for the review
|
|
22
|
+
itself.
|
|
23
|
+
|
|
24
|
+
The script prepares each review's workspace before the agent starts: the pull
|
|
25
|
+
request's head commit is downloaded as a tarball and extracted to a directory of
|
|
26
|
+
its own, which becomes the conversation's working directory. The agent is told
|
|
27
|
+
not to clone, fetch, check out, or delete anything, and the script removes the
|
|
28
|
+
checkout once the conversation has stopped. Nothing accumulates between runs.
|
|
22
29
|
|
|
23
30
|
---
|
|
24
31
|
|
|
@@ -31,7 +38,13 @@ Verify that the following secret is set in **OpenHands Settings -> Secrets**:
|
|
|
31
38
|
| Secret name | Token type | Minimum permissions |
|
|
32
39
|
|---|---|---|
|
|
33
40
|
| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` for private repos or `public_repo` for public repos |
|
|
34
|
-
| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: Read
|
|
41
|
+
| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Contents: Read, Metadata: Read, Pull requests: **Read and Write**, Issues: Read and Write |
|
|
42
|
+
|
|
43
|
+
Pull-request **write** access is required because the agent publishes a pull
|
|
44
|
+
request review, not just an issue comment. A token with only Pull requests: Read
|
|
45
|
+
will poll happily and then fail at the point of publishing.
|
|
46
|
+
|
|
47
|
+
When several repositories are monitored, the token must cover all of them.
|
|
35
48
|
|
|
36
49
|
Check with:
|
|
37
50
|
```bash
|
|
@@ -57,12 +70,13 @@ Run the `curl` check above.
|
|
|
57
70
|
- If the API returns `{"message": "Bad credentials"}`: tell the user the
|
|
58
71
|
token is invalid and ask them to update it. Stop.
|
|
59
72
|
|
|
60
|
-
### Step 2 - Collect
|
|
73
|
+
### Step 2 - Collect repositories
|
|
61
74
|
|
|
62
|
-
Ask: *"Which GitHub
|
|
63
|
-
(Format: `owner/repo`, e.g. `myorg/backend
|
|
75
|
+
Ask: *"Which GitHub repositories should be monitored?
|
|
76
|
+
(Format: `owner/repo`, e.g. `myorg/backend`. List several separated by commas to
|
|
77
|
+
review them all from one automation.)"*
|
|
64
78
|
|
|
65
|
-
Validate access:
|
|
79
|
+
Validate access to **each** repository:
|
|
66
80
|
```bash
|
|
67
81
|
curl -s "https://api.github.com/repos/{owner}/{repo}" \
|
|
68
82
|
-H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
|
|
@@ -76,7 +90,13 @@ else:
|
|
|
76
90
|
"
|
|
77
91
|
```
|
|
78
92
|
|
|
79
|
-
Record `
|
|
93
|
+
Record every accepted repository into `REPOS = ["{owner}/{repo}", ...]`. If one
|
|
94
|
+
repository fails the check, say which and ask whether to continue without it.
|
|
95
|
+
|
|
96
|
+
Each repository is polled independently and keeps its own state, so pull-request
|
|
97
|
+
numbers never collide between them. The trigger label, tone, and schedule are
|
|
98
|
+
shared by all of them; a repository needing different settings wants its own
|
|
99
|
+
automation.
|
|
80
100
|
|
|
81
101
|
### Step 3 - Collect trigger label
|
|
82
102
|
|
|
@@ -122,9 +142,15 @@ Record as `CRON_SCHEDULE`.
|
|
|
122
142
|
Read `scripts/main.py` from this skill's directory. Apply exactly five constant
|
|
123
143
|
substitutions near the top of the file:
|
|
124
144
|
|
|
145
|
+
> The script also reads a `config.json` shipped beside it, if there is one, over
|
|
146
|
+
> these constants. That is how the catalog entry
|
|
147
|
+
> (`automations/catalog/github-pr-reviewer/`) configures an unmodified copy,
|
|
148
|
+
> since a declarative host cannot rewrite Python. This setup path substitutes the
|
|
149
|
+
> constants and ships no `config.json`, so the two never collide.
|
|
150
|
+
|
|
125
151
|
| Placeholder | Replace with |
|
|
126
152
|
|---|---|
|
|
127
|
-
| `
|
|
153
|
+
| `REPOS = ["owner/repo"]` | `REPOS = ["{owner_repo}", ...]` - one entry per repository collected in Step 2 |
|
|
128
154
|
| `TRIGGER_LABEL = "openhands-review"` | `TRIGGER_LABEL = "{trigger_label}"` |
|
|
129
155
|
| `REVIEW_TONE = "thorough"` | `REVIEW_TONE = "{review_tone}"` |
|
|
130
156
|
| `REVIEW_STYLE_INSTRUCTIONS = ""` | `REVIEW_STYLE_INSTRUCTIONS = "{style_instructions}"` |
|
|
@@ -132,6 +158,7 @@ substitutions near the top of the file:
|
|
|
132
158
|
|
|
133
159
|
Use a safe string writer such as `json.dumps(value)` when inserting user-provided
|
|
134
160
|
repository names, labels, or style instructions into Python string literals.
|
|
161
|
+
`json.dumps(list_of_repos)` produces the whole `REPOS` list safely in one step.
|
|
135
162
|
|
|
136
163
|
Write the customized script to a temporary build directory:
|
|
137
164
|
```bash
|
|
@@ -173,14 +200,19 @@ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
|
|
|
173
200
|
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
|
|
174
201
|
-H "Content-Type: application/json" \
|
|
175
202
|
-d "{
|
|
176
|
-
\"name\": \"GitHub PR Reviewer: {
|
|
203
|
+
\"name\": \"GitHub PR Reviewer: {repo_summary} label {trigger_label}\",
|
|
177
204
|
\"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\"},
|
|
178
205
|
\"tarball_path\": \"$TARBALL_PATH\",
|
|
179
206
|
\"entrypoint\": \"python3 main.py\",
|
|
180
|
-
\"timeout\":
|
|
207
|
+
\"timeout\": 600
|
|
181
208
|
}" | python3 -m json.tool
|
|
182
209
|
```
|
|
183
210
|
|
|
211
|
+
Use the single repository as `{repo_summary}` when there is one, and something
|
|
212
|
+
like `3 repos` when there are several. A poll now downloads a tarball per queued
|
|
213
|
+
review, so the timeout allows for that; a run never waits for a review to
|
|
214
|
+
finish, only for it to be started.
|
|
215
|
+
|
|
184
216
|
Record the returned `id`.
|
|
185
217
|
|
|
186
218
|
### Step 9 - Confirm
|
|
@@ -190,44 +222,67 @@ Tell the user:
|
|
|
190
222
|
> ✅ **GitHub PR Reviewer** is running!
|
|
191
223
|
>
|
|
192
224
|
> - Automation ID: `{id}`
|
|
193
|
-
> -
|
|
225
|
+
> - Repositories: `{owner}/{repo}`, ... (one line each)
|
|
194
226
|
> - Trigger label: `{trigger_label}`
|
|
195
227
|
> - Review tone: `{tone}`
|
|
196
228
|
> - Polling schedule: `{cron_schedule}`
|
|
197
|
-
> - State file:
|
|
229
|
+
> - State file per repository:
|
|
230
|
+
> `~/.openhands/workspaces/automation-state/github_pr_reviewer_label_event_{id}_{owner}__{repo}.json`
|
|
198
231
|
>
|
|
199
232
|
> Apply the `{trigger_label}` label to a pull request to queue a review. Each
|
|
200
233
|
> label event is processed once. To request another review, remove and re-apply
|
|
201
234
|
> the label.
|
|
235
|
+
>
|
|
236
|
+
> The review is published as a pull request review on the head commit, with
|
|
237
|
+
> inline comments where a finding maps to a changed line.
|
|
202
238
|
|
|
203
239
|
---
|
|
204
240
|
|
|
205
241
|
## Runtime Behaviour (per poll)
|
|
206
242
|
|
|
207
|
-
Each cron run executes `main.py`, which
|
|
243
|
+
Each cron run executes `main.py`, which resolves and validates
|
|
244
|
+
`GITHUB_PERSONAL_ACCESS_TOKEN` once, then processes every repository in `REPOS`
|
|
245
|
+
independently. One repository failing does not stop the others; the run fails
|
|
246
|
+
only if every repository fails.
|
|
247
|
+
|
|
248
|
+
For each repository:
|
|
208
249
|
|
|
209
|
-
1. Loads
|
|
210
|
-
2.
|
|
250
|
+
1. Loads that repository's state (see `references/state-schema.md`).
|
|
251
|
+
2. Verifies repository access.
|
|
211
252
|
3. Lists open PRs, newest-updated first.
|
|
212
253
|
4. For each open PR carrying `TRIGGER_LABEL`:
|
|
213
254
|
- Refetches current PR metadata to avoid acting on stale list data.
|
|
214
255
|
- Finds the latest matching GitHub `labeled` issue event.
|
|
215
256
|
- Skips the event if it has already been tracked.
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
- Downloads the PR's head commit as a tarball and extracts it to
|
|
258
|
+
`{WORKSPACE_BASE}/repositories/{owner}__{repo}/pr-{number}-{sha12}`. The
|
|
259
|
+
archive is checked as it is unpacked: a single root, no absolute or `..`
|
|
260
|
+
paths, and symlinks skipped rather than materialised.
|
|
261
|
+
- Starts an OpenHands conversation **whose working directory is that
|
|
262
|
+
checkout**, with a review prompt carrying PR metadata, the exact head SHA,
|
|
263
|
+
and label event details.
|
|
220
264
|
- Posts an acknowledgement comment with the label event, head SHA, and
|
|
221
265
|
conversation link.
|
|
222
|
-
- Records the
|
|
266
|
+
- Records the review in state with `status: "active"` and the checkout path.
|
|
267
|
+
- If the checkout or the conversation cannot be created, the checkout is
|
|
268
|
+
removed and nothing is recorded, so the next poll retries the label event.
|
|
223
269
|
5. For each active review conversation:
|
|
224
270
|
- Marks it closed without posting if the PR has closed or merged.
|
|
225
271
|
- Suppresses stale results if the PR head SHA changed after the review was
|
|
226
272
|
queued.
|
|
227
273
|
- When the conversation reaches `idle`, `finished`, `error`, or `stuck`,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
274
|
+
asks GitHub whether a review by the token's own user exists for that head
|
|
275
|
+
SHA. If it does, the review is complete. If it does not, the agent's final
|
|
276
|
+
response is posted as a comment so the work is not lost.
|
|
277
|
+
- Abandons a conversation that has not reached a terminal status within two
|
|
278
|
+
hours, so its checkout can be reclaimed.
|
|
279
|
+
6. Removes the checkout of every finished review, but only after confirming the
|
|
280
|
+
conversation has stopped - deleting it under a running agent would remove its
|
|
281
|
+
working directory. When that cannot be confirmed the directory is left alone
|
|
282
|
+
and the next poll tries again.
|
|
283
|
+
7. Saves that repository's state atomically.
|
|
284
|
+
|
|
285
|
+
The completion callback fires once for the whole run.
|
|
231
286
|
|
|
232
287
|
---
|
|
233
288
|
|
|
@@ -237,6 +292,9 @@ Each cron run executes `main.py`, which:
|
|
|
237
292
|
review lifecycle diagram.
|
|
238
293
|
- **`scripts/main.py`** - The complete automation script. Customize the five
|
|
239
294
|
constants at the top before packaging.
|
|
295
|
+
- **`tests/test_main.py`** - Unit tests for the checkout, its removal, and state
|
|
296
|
+
handling. Run them from the skill root with `python -m pytest tests/` after
|
|
297
|
+
editing the script.
|
|
240
298
|
|
|
241
299
|
---
|
|
242
300
|
|
|
@@ -246,7 +304,11 @@ Each cron run executes `main.py`, which:
|
|
|
246
304
|
|---|---|---|
|
|
247
305
|
| Bot never queues reviews | Trigger label not present or no matching `labeled` event | Apply the configured label to the PR |
|
|
248
306
|
| "Bad credentials" in run logs | Token expired | Rotate and update `GITHUB_PERSONAL_ACCESS_TOKEN` |
|
|
249
|
-
| 404 on repo access | Repo name wrong or no access | Re-check `
|
|
307
|
+
| 404 on repo access | Repo name wrong or no access | Re-check the entry in `REPOS` and the token's permissions |
|
|
308
|
+
| One repository is skipped, others work | That repository failed its access check | Read the `=== owner/repo ===` block in the run log |
|
|
250
309
|
| Same PR not reviewed after new commits | Label event was already processed | Remove and re-apply the trigger label |
|
|
251
310
|
| Review result never posts | Conversation still running or stuck | Open the conversation link from the acknowledgement comment |
|
|
252
311
|
| Stale review suppressed | PR head SHA changed while the agent was reviewing | Re-apply the trigger label after the latest commit |
|
|
312
|
+
| Review arrives as a plain comment, not a review | Publishing failed, so the script posted the text as a fallback | Check that the token has Pull requests: Read and Write |
|
|
313
|
+
| Agent reports it cannot clone the repo | Prompt asked it not to; the workspace is already the checkout | No action - the code is at the head SHA in its working directory |
|
|
314
|
+
| Checkouts remain under `repositories/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# auto-generated by sync_extensions.py
|
|
3
|
-
description: Create an automation that reviews GitHub pull requests when a configurable trigger label is applied. Polls
|
|
3
|
+
description: Create an automation that reviews GitHub pull requests when a configurable trigger label is applied. Polls one or more repositories deterministically, starts one OpenHands review conversation per label event with the pull request's head commit already checked out, and publishes the review to GitHub.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
|