@openhands/extensions 0.21.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +881 -0
- package/.codex-plugin/marketplace.json +881 -0
- package/.github/workflows/npm-publish.yml +9 -3
- package/.plugin/marketplace.json +881 -0
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/bundle-index.js +16 -7
- package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
- package/automations/catalog/github-issue-to-pr/manifest.json +23 -15
- package/automations/catalog/github-issue-triage/manifest.json +4 -3
- package/automations/catalog/github-pr-reviewer/manifest.json +22 -15
- package/automations/catalog-index.js +6 -4
- package/marketplaces/openhands-extensions.json +6 -0
- package/package.json +1 -1
- package/plugins/city-weather/.claude-plugin/plugin.json +13 -0
- package/plugins/city-weather/.codex-plugin/plugin.json +13 -0
- package/plugins/cobol-modernization/.claude-plugin/plugin.json +19 -0
- package/plugins/cobol-modernization/.codex-plugin/plugin.json +19 -0
- package/plugins/issue-duplicate-checker/.claude-plugin/plugin.json +13 -0
- package/plugins/issue-duplicate-checker/.codex-plugin/plugin.json +13 -0
- package/plugins/magic-test/.claude-plugin/plugin.json +13 -0
- package/plugins/magic-test/.codex-plugin/plugin.json +13 -0
- package/plugins/migration-scoring/.claude-plugin/plugin.json +19 -0
- package/plugins/migration-scoring/.codex-plugin/plugin.json +19 -0
- package/plugins/onboarding/.claude-plugin/plugin.json +20 -0
- package/plugins/onboarding/.codex-plugin/plugin.json +20 -0
- package/plugins/onboarding/skills/agent-readiness-report/references/criteria.md +144 -0
- package/plugins/onboarding/skills/improve-agent-readiness/references/criteria.md +144 -0
- package/plugins/openhands/.claude-plugin/plugin.json +13 -0
- package/plugins/openhands/.codex-plugin/plugin.json +13 -0
- package/plugins/openhands/skills/openhands-api/.claude-plugin/plugin.json +22 -0
- package/plugins/openhands/skills/openhands-api/.codex-plugin/plugin.json +22 -0
- package/plugins/openhands/skills/openhands-api/.plugin/plugin.json +22 -0
- package/plugins/openhands/skills/openhands-api/README.md +52 -0
- package/plugins/openhands/skills/openhands-api/SKILL.md +469 -0
- package/plugins/openhands/skills/openhands-api/references/README.md +33 -0
- package/plugins/openhands/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
- package/plugins/openhands/skills/openhands-api/references/example_prompt.md +12 -0
- package/plugins/openhands/skills/openhands-api/references/windows.md +44 -0
- package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +606 -0
- package/plugins/openhands/skills/openhands-api/scripts/openhands_api.ts +252 -0
- package/plugins/openhands/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
- package/plugins/openhands/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
- package/plugins/openhands/skills/openhands-automation/.plugin/plugin.json +19 -0
- package/plugins/openhands/skills/openhands-automation/README.md +89 -0
- package/plugins/openhands/skills/openhands-automation/SKILL.md +899 -0
- package/plugins/openhands/skills/openhands-automation/commands/automation-create.md +8 -0
- package/plugins/openhands/skills/openhands-automation/references/ab-testing.md +185 -0
- package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +832 -0
- package/plugins/openhands/skills/openhands-automation/references/security.md +84 -0
- package/plugins/openhands/skills/openhands-automation/references/windows.md +57 -0
- package/plugins/openhands/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
- package/plugins/openhands/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
- package/plugins/openhands/skills/openhands-sdk/.plugin/plugin.json +20 -0
- package/plugins/openhands/skills/openhands-sdk/README.md +22 -0
- package/plugins/openhands/skills/openhands-sdk/SKILL.md +245 -0
- package/plugins/openhands/skills/openhands-sdk/commands/sdk.md +8 -0
- package/plugins/pr-review/.claude-plugin/plugin.json +13 -0
- package/plugins/pr-review/.codex-plugin/plugin.json +13 -0
- package/plugins/pr-review/skills/code-review/.claude-plugin/plugin.json +19 -0
- package/plugins/pr-review/skills/code-review/.codex-plugin/plugin.json +19 -0
- package/plugins/pr-review/skills/code-review/.plugin/plugin.json +19 -0
- package/plugins/pr-review/skills/code-review/README.md +18 -0
- package/plugins/pr-review/skills/code-review/SKILL.md +209 -0
- package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +8 -0
- package/plugins/pr-review/skills/code-review/commands/codereview.md +8 -0
- package/plugins/pr-review/skills/code-review/references/risk-evaluation.md +41 -0
- package/plugins/pr-review/skills/code-review/references/supply-chain-security.md +31 -0
- package/plugins/pr-review/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
- package/plugins/pr-review/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
- package/plugins/pr-review/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/plugins/pr-review/skills/github-pr-review/README.md +145 -0
- package/plugins/pr-review/skills/github-pr-review/SKILL.md +187 -0
- package/plugins/pr-review/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/plugins/pr-review/skills/github-pr-review/references/windows.md +47 -0
- package/plugins/qa-changes/.claude-plugin/plugin.json +11 -0
- package/plugins/qa-changes/.codex-plugin/plugin.json +11 -0
- package/plugins/qa-changes/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
- package/plugins/qa-changes/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
- package/plugins/qa-changes/skills/github-pr-review/.plugin/plugin.json +18 -0
- package/plugins/qa-changes/skills/github-pr-review/README.md +145 -0
- package/plugins/qa-changes/skills/github-pr-review/SKILL.md +187 -0
- package/plugins/qa-changes/skills/github-pr-review/commands/github-pr-review.md +8 -0
- package/plugins/qa-changes/skills/github-pr-review/references/windows.md +47 -0
- package/plugins/qa-changes/skills/qa-changes/README.md +18 -0
- package/plugins/qa-changes/skills/qa-changes/SKILL.md +229 -0
- package/plugins/qa-changes/skills/qa-changes/commands/qa-changes.md +8 -0
- package/plugins/release-notes/.claude-plugin/plugin.json +19 -0
- package/plugins/release-notes/.codex-plugin/plugin.json +19 -0
- package/plugins/release-notes/skills/release-notes/README.md +24 -0
- package/plugins/release-notes/skills/release-notes/SKILL.md +19 -0
- package/plugins/release-notes/skills/release-notes/commands/release-notes.md +8 -0
- package/plugins/vulnerability-remediation/.claude-plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/.codex-plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/skills/github/.claude-plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/skills/github/.codex-plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/skills/github/.plugin/plugin.json +19 -0
- package/plugins/vulnerability-remediation/skills/github/README.md +42 -0
- package/plugins/vulnerability-remediation/skills/github/SKILL.md +107 -0
- package/plugins/vulnerability-remediation/skills/github/references/windows.md +48 -0
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +150 -0
- package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +230 -0
- package/plugins/vulnerability-remediation/skills/security/.claude-plugin/plugin.json +18 -0
- package/plugins/vulnerability-remediation/skills/security/.codex-plugin/plugin.json +18 -0
- package/plugins/vulnerability-remediation/skills/security/.plugin/plugin.json +18 -0
- package/plugins/vulnerability-remediation/skills/security/README.md +38 -0
- package/plugins/vulnerability-remediation/skills/security/SKILL.md +33 -0
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/add-javadoc/.claude-plugin/plugin.json +18 -0
- package/skills/add-javadoc/.codex-plugin/plugin.json +18 -0
- package/skills/add-skill/.claude-plugin/plugin.json +18 -0
- package/skills/add-skill/.codex-plugin/plugin.json +18 -0
- package/skills/agent-canvas-environment/.claude-plugin/plugin.json +19 -0
- package/skills/agent-canvas-environment/.codex-plugin/plugin.json +19 -0
- package/skills/agent-creator/.claude-plugin/plugin.json +20 -0
- package/skills/agent-creator/.codex-plugin/plugin.json +20 -0
- package/skills/agent-memory/.claude-plugin/plugin.json +18 -0
- package/skills/agent-memory/.codex-plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/.claude-plugin/plugin.json +18 -0
- package/skills/agent-sdk-builder/.codex-plugin/plugin.json +18 -0
- package/skills/azure-devops/.claude-plugin/plugin.json +18 -0
- package/skills/azure-devops/.codex-plugin/plugin.json +18 -0
- package/skills/bitbucket/.claude-plugin/plugin.json +17 -0
- package/skills/bitbucket/.codex-plugin/plugin.json +17 -0
- package/skills/bitbucket-cloud/.claude-plugin/plugin.json +18 -0
- package/skills/bitbucket-cloud/.codex-plugin/plugin.json +18 -0
- package/skills/bitbucket-data-center/.claude-plugin/plugin.json +19 -0
- package/skills/bitbucket-data-center/.codex-plugin/plugin.json +19 -0
- package/skills/code-review/.claude-plugin/plugin.json +19 -0
- package/skills/code-review/.codex-plugin/plugin.json +19 -0
- package/skills/code-simplifier/.claude-plugin/plugin.json +21 -0
- package/skills/code-simplifier/.codex-plugin/plugin.json +21 -0
- package/skills/datadog/.claude-plugin/plugin.json +19 -0
- package/skills/datadog/.codex-plugin/plugin.json +19 -0
- package/skills/deno/.claude-plugin/plugin.json +18 -0
- package/skills/deno/.codex-plugin/plugin.json +18 -0
- package/skills/discord/.claude-plugin/plugin.json +18 -0
- package/skills/discord/.codex-plugin/plugin.json +18 -0
- package/skills/docker/.claude-plugin/plugin.json +17 -0
- package/skills/docker/.codex-plugin/plugin.json +17 -0
- package/skills/evidence-based-citations/.claude-plugin/plugin.json +20 -0
- package/skills/evidence-based-citations/.codex-plugin/plugin.json +20 -0
- package/skills/flarglebargle/.claude-plugin/plugin.json +16 -0
- package/skills/flarglebargle/.codex-plugin/plugin.json +16 -0
- package/skills/frontend-design/.claude-plugin/plugin.json +21 -0
- package/skills/frontend-design/.codex-plugin/plugin.json +21 -0
- package/skills/github/.claude-plugin/plugin.json +19 -0
- package/skills/github/.codex-plugin/plugin.json +19 -0
- package/skills/github/scripts/agent_conversation.py +150 -0
- package/skills/github/scripts/github_client.py +31 -5
- package/skills/github-actions/.claude-plugin/plugin.json +19 -0
- package/skills/github-actions/.codex-plugin/plugin.json +19 -0
- package/skills/github-agents-md-maintainer/.claude-plugin/plugin.json +19 -0
- package/skills/github-agents-md-maintainer/.codex-plugin/plugin.json +19 -0
- package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
- package/skills/github-delivery-watchdog/.claude-plugin/plugin.json +9 -0
- package/skills/github-delivery-watchdog/.codex-plugin/plugin.json +9 -0
- package/skills/github-delivery-watchdog/.plugin/plugin.json +9 -0
- package/skills/github-delivery-watchdog/README.md +10 -0
- package/skills/github-delivery-watchdog/SKILL.md +35 -0
- package/skills/github-delivery-watchdog/commands/github-delivery-watchdog.md +8 -0
- package/skills/github-delivery-watchdog/scripts/github_client.py +230 -0
- package/skills/github-delivery-watchdog/scripts/worker.py +96 -0
- package/skills/github-issue-to-pr/.claude-plugin/plugin.json +19 -0
- package/skills/github-issue-to-pr/.codex-plugin/plugin.json +19 -0
- package/skills/github-issue-to-pr/SKILL.md +7 -0
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +150 -0
- package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
- package/skills/github-issue-to-pr/scripts/main.py +16 -10
- package/skills/github-issue-to-pr/scripts/worker.py +172 -0
- package/skills/github-issue-triage/.claude-plugin/plugin.json +9 -0
- package/skills/github-issue-triage/.codex-plugin/plugin.json +9 -0
- package/skills/github-issue-triage/SKILL.md +15 -18
- package/skills/github-issue-triage/scripts/agent_conversation.py +150 -0
- package/skills/github-issue-triage/scripts/github_client.py +230 -0
- package/skills/github-issue-triage/scripts/worker.py +81 -98
- package/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
- package/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
- package/skills/github-pr-reviewer/.claude-plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/.codex-plugin/plugin.json +20 -0
- package/skills/github-pr-reviewer/SKILL.md +7 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +150 -0
- package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
- package/skills/github-pr-reviewer/scripts/main.py +20 -8
- package/skills/github-pr-reviewer/scripts/worker.py +94 -0
- package/skills/github-repo-monitor/.claude-plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/.codex-plugin/plugin.json +22 -0
- package/skills/github-repo-monitor/scripts/github_client.py +230 -0
- package/skills/gitlab/.claude-plugin/plugin.json +17 -0
- package/skills/gitlab/.codex-plugin/plugin.json +17 -0
- package/skills/gitlab-issue-to-mr/.claude-plugin/plugin.json +19 -0
- package/skills/gitlab-issue-to-mr/.codex-plugin/plugin.json +19 -0
- package/skills/incident-retrospective/.claude-plugin/plugin.json +21 -0
- package/skills/incident-retrospective/.codex-plugin/plugin.json +21 -0
- package/skills/index.js +12 -3
- package/skills/iterate/.claude-plugin/plugin.json +13 -0
- package/skills/iterate/.codex-plugin/plugin.json +13 -0
- package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +19 -0
- package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +19 -0
- package/skills/jupyter/.claude-plugin/plugin.json +18 -0
- package/skills/jupyter/.codex-plugin/plugin.json +18 -0
- package/skills/kubernetes/.claude-plugin/plugin.json +18 -0
- package/skills/kubernetes/.codex-plugin/plugin.json +18 -0
- package/skills/learn-from-code-review/.claude-plugin/plugin.json +19 -0
- package/skills/learn-from-code-review/.codex-plugin/plugin.json +19 -0
- package/skills/linear/.claude-plugin/plugin.json +19 -0
- package/skills/linear/.codex-plugin/plugin.json +19 -0
- package/skills/linear-triage/.claude-plugin/plugin.json +21 -0
- package/skills/linear-triage/.codex-plugin/plugin.json +21 -0
- package/skills/news-digest/.claude-plugin/plugin.json +19 -0
- package/skills/news-digest/.codex-plugin/plugin.json +19 -0
- package/skills/notion/.claude-plugin/plugin.json +17 -0
- package/skills/notion/.codex-plugin/plugin.json +17 -0
- package/skills/npm/.claude-plugin/plugin.json +17 -0
- package/skills/npm/.codex-plugin/plugin.json +17 -0
- package/skills/openhands-api/.claude-plugin/plugin.json +22 -0
- package/skills/openhands-api/.codex-plugin/plugin.json +22 -0
- package/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
- package/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
- package/skills/openhands-enterprise-troubleshooting/.claude-plugin/plugin.json +20 -0
- package/skills/openhands-enterprise-troubleshooting/.codex-plugin/plugin.json +20 -0
- package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
- package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
- package/skills/pdflatex/.claude-plugin/plugin.json +18 -0
- package/skills/pdflatex/.codex-plugin/plugin.json +18 -0
- package/skills/plain-english-content/.claude-plugin/plugin.json +15 -0
- package/skills/plain-english-content/.codex-plugin/plugin.json +15 -0
- package/skills/prd/.claude-plugin/plugin.json +19 -0
- package/skills/prd/.codex-plugin/plugin.json +19 -0
- package/skills/research-brief/.claude-plugin/plugin.json +20 -0
- package/skills/research-brief/.codex-plugin/plugin.json +20 -0
- package/skills/security/.claude-plugin/plugin.json +18 -0
- package/skills/security/.codex-plugin/plugin.json +18 -0
- package/skills/skill-creator/.claude-plugin/plugin.json +17 -0
- package/skills/skill-creator/.codex-plugin/plugin.json +17 -0
- package/skills/slack-channel-monitor/.claude-plugin/plugin.json +21 -0
- package/skills/slack-channel-monitor/.codex-plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/.claude-plugin/plugin.json +21 -0
- package/skills/slack-standup-digest/.codex-plugin/plugin.json +21 -0
- package/skills/spark-version-upgrade/.claude-plugin/plugin.json +20 -0
- package/skills/spark-version-upgrade/.codex-plugin/plugin.json +20 -0
- package/skills/ssh/.claude-plugin/plugin.json +18 -0
- package/skills/ssh/.codex-plugin/plugin.json +18 -0
- package/skills/swift-linux/.claude-plugin/plugin.json +17 -0
- package/skills/swift-linux/.codex-plugin/plugin.json +17 -0
- package/skills/technical-writing/.claude-plugin/plugin.json +15 -0
- package/skills/technical-writing/.codex-plugin/plugin.json +15 -0
- package/skills/theme-factory/.claude-plugin/plugin.json +19 -0
- package/skills/theme-factory/.codex-plugin/plugin.json +19 -0
- package/skills/ticket-to-code-change/.claude-plugin/plugin.json +21 -0
- package/skills/ticket-to-code-change/.codex-plugin/plugin.json +21 -0
- package/skills/upstream-fork-sync/.claude-plugin/plugin.json +21 -0
- package/skills/upstream-fork-sync/.codex-plugin/plugin.json +21 -0
- package/skills/uv/.claude-plugin/plugin.json +18 -0
- package/skills/uv/.codex-plugin/plugin.json +18 -0
- package/skills/vercel/.claude-plugin/plugin.json +18 -0
- package/skills/vercel/.codex-plugin/plugin.json +18 -0
- package/tests/fixtures/automations/capabilities.json +6 -3
- package/tests/fixtures/automations/github-pr-reviewer.json +64 -43
- package/tests/github_automation_helpers.py +7 -1
- package/tests/test_agent_conversation_dispatch.py +196 -0
- package/tests/test_automation_setup.py +3 -3
- package/tests/test_github_developer_delivery.py +150 -0
- package/tests/test_github_reviewer_delivery.py +110 -0
- package/tests/test_github_triage_delivery.py +78 -41
- package/tests/test_github_watchdog_delivery.py +233 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Shared GitHub transport and repository operations for GitHub automations."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from urllib.error import HTTPError
|
|
11
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_secret(name: str) -> str:
|
|
16
|
+
"""Read one named secret from the environment or configured Agent Server."""
|
|
17
|
+
value = os.environ.get(name)
|
|
18
|
+
if value:
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
from openhands.sdk.workspace import RemoteWorkspace
|
|
22
|
+
|
|
23
|
+
workspace = RemoteWorkspace(
|
|
24
|
+
host=os.environ["AGENT_SERVER_URL"],
|
|
25
|
+
api_key=os.environ["SESSION_API_KEY"],
|
|
26
|
+
working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
|
|
27
|
+
)
|
|
28
|
+
try:
|
|
29
|
+
secret = workspace.get_secrets([name]).get(name)
|
|
30
|
+
value = secret.get_value() if secret else None
|
|
31
|
+
finally:
|
|
32
|
+
workspace.reset_client()
|
|
33
|
+
if not value:
|
|
34
|
+
raise ValueError(f"The GitHub credential {name} is unavailable")
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def github_request(
|
|
39
|
+
token: str,
|
|
40
|
+
method: str,
|
|
41
|
+
path: str,
|
|
42
|
+
params: dict | None = None,
|
|
43
|
+
body: dict | None = None,
|
|
44
|
+
accept: str = "application/vnd.github+json",
|
|
45
|
+
) -> tuple:
|
|
46
|
+
url = f"https://api.github.com{path}"
|
|
47
|
+
if params:
|
|
48
|
+
url = f"{url}?{urlencode(params)}"
|
|
49
|
+
headers = {
|
|
50
|
+
"Authorization": f"Bearer {token}",
|
|
51
|
+
"Accept": accept,
|
|
52
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}
|
|
55
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
56
|
+
req = Request(url, data=data, headers=headers, method=method)
|
|
57
|
+
with urlopen(req, timeout=90) as r:
|
|
58
|
+
raw = r.read()
|
|
59
|
+
return (json.loads(raw) if raw.strip() else {}), dict(r.headers)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def github_paginate(token: str, path: str, params: dict | None = None) -> list:
|
|
63
|
+
results = []
|
|
64
|
+
base_params = dict(params or {})
|
|
65
|
+
base_params.setdefault("per_page", 100)
|
|
66
|
+
for page in range(1, 101):
|
|
67
|
+
base_params["page"] = page
|
|
68
|
+
data, _ = github_request(token, "GET", path, params=base_params)
|
|
69
|
+
if not isinstance(data, list):
|
|
70
|
+
raise TypeError("Expected a paginated GitHub list")
|
|
71
|
+
results.extend(data)
|
|
72
|
+
if len(data) < int(base_params["per_page"]):
|
|
73
|
+
return results
|
|
74
|
+
raise RuntimeError("GitHub pagination exceeded limit")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GitHubRepository:
|
|
78
|
+
name = "GitHub automation"
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
config_path=Path("config.json"),
|
|
83
|
+
*,
|
|
84
|
+
github_token_secret,
|
|
85
|
+
repository=None,
|
|
86
|
+
conversation=None,
|
|
87
|
+
dispatcher=None,
|
|
88
|
+
):
|
|
89
|
+
self.config = json.loads(Path(config_path).read_text())
|
|
90
|
+
self.repository = repository or self.config["repository"]
|
|
91
|
+
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", self.repository):
|
|
92
|
+
raise ValueError("repository must be owner/repo")
|
|
93
|
+
if not re.fullmatch(r"[A-Z_][A-Z0-9_]*", github_token_secret):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"Expected the environment variable containing the GitHub token"
|
|
96
|
+
)
|
|
97
|
+
self.token_name = github_token_secret
|
|
98
|
+
self.token = _load_secret(github_token_secret)
|
|
99
|
+
self.conversation = conversation
|
|
100
|
+
self.conversation_id = str(conversation.id) if conversation else None
|
|
101
|
+
self.dispatcher = dispatcher
|
|
102
|
+
self.workspace = Path(os.environ["WORKSPACE_BASE"])
|
|
103
|
+
self.project = self.workspace
|
|
104
|
+
self.evidence = self.workspace / "evidence"
|
|
105
|
+
self.evidence.mkdir(exist_ok=True)
|
|
106
|
+
self._completed_dependencies = {}
|
|
107
|
+
|
|
108
|
+
@cached_property
|
|
109
|
+
def base_branch(self):
|
|
110
|
+
return self.config.get("base_branch") or self.gh("GET", "")["default_branch"]
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def github_instructions(self):
|
|
114
|
+
return (
|
|
115
|
+
f"Use `GH_TOKEN=${self.token_name} gh api` for GitHub requests. "
|
|
116
|
+
"Never print the credential value. "
|
|
117
|
+
f"Only {self.repository} is in scope. Work in {self.project}. "
|
|
118
|
+
"Do not modify the automation bundle or its configuration."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def gh(self, method, path, body=None):
|
|
122
|
+
return github_request(
|
|
123
|
+
self.token, method, f"/repos/{self.repository}" + path, body=body
|
|
124
|
+
)[0]
|
|
125
|
+
|
|
126
|
+
def shell(self, args, cwd=None, timeout=300):
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
args,
|
|
129
|
+
cwd=cwd or self.project,
|
|
130
|
+
text=True,
|
|
131
|
+
stdout=subprocess.PIPE,
|
|
132
|
+
stderr=subprocess.STDOUT,
|
|
133
|
+
timeout=timeout,
|
|
134
|
+
check=False,
|
|
135
|
+
)
|
|
136
|
+
if result.returncode:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"{args[0]} failed: {result.stdout[-4000:].replace(self.token, '[REDACTED]')}"
|
|
139
|
+
)
|
|
140
|
+
return result.stdout.strip()
|
|
141
|
+
|
|
142
|
+
def comment(self, number, text):
|
|
143
|
+
return self.gh(
|
|
144
|
+
"POST",
|
|
145
|
+
f"/issues/{number}/comments",
|
|
146
|
+
{
|
|
147
|
+
"body": text
|
|
148
|
+
+ f"\n\nFactory role: `{self.name}`; conversation: `{self.conversation_id}`."
|
|
149
|
+
+ "\n\n_This comment was posted by an AI agent (OpenHands)._"
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def open_issues(self):
|
|
154
|
+
return [
|
|
155
|
+
i for i in self.gh_pages("/issues?state=open") if "pull_request" not in i
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def statuses(self, sha):
|
|
159
|
+
result = {}
|
|
160
|
+
for item in self.gh_pages(f"/commits/{sha}/statuses"):
|
|
161
|
+
result.setdefault(item["context"], item["state"])
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def completed_dependency(self, number):
|
|
165
|
+
if number in self._completed_dependencies:
|
|
166
|
+
return self._completed_dependencies[number]
|
|
167
|
+
try:
|
|
168
|
+
dependency = self.gh("GET", f"/issues/{number}")
|
|
169
|
+
except HTTPError as exc:
|
|
170
|
+
if exc.code == 404:
|
|
171
|
+
return False
|
|
172
|
+
raise
|
|
173
|
+
completed = (
|
|
174
|
+
dependency["state"] == "closed"
|
|
175
|
+
and dependency.get("state_reason") == "completed"
|
|
176
|
+
)
|
|
177
|
+
self._completed_dependencies[number] = completed
|
|
178
|
+
return completed
|
|
179
|
+
|
|
180
|
+
def dependencies_complete(self, issue):
|
|
181
|
+
"""Honor explicit Depends on lines; unknown/incomplete issues remain blocked."""
|
|
182
|
+
for line in re.findall(
|
|
183
|
+
"^Depends on:\\s*(.+)$",
|
|
184
|
+
issue.get("body") or "",
|
|
185
|
+
re.MULTILINE | re.IGNORECASE,
|
|
186
|
+
):
|
|
187
|
+
for number in re.findall("#(\\d+)", line):
|
|
188
|
+
if not self.completed_dependency(number):
|
|
189
|
+
return False
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def gh_pages(self, endpoint):
|
|
193
|
+
split = urlsplit(endpoint)
|
|
194
|
+
return github_paginate(
|
|
195
|
+
self.token,
|
|
196
|
+
f"/repos/{self.repository}" + split.path,
|
|
197
|
+
params=dict(parse_qsl(split.query)),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def run_repositories(automation_type, conversation=None, dispatcher=None):
|
|
202
|
+
parser = argparse.ArgumentParser(description=automation_type.__doc__)
|
|
203
|
+
parser.add_argument("--github-token-secret")
|
|
204
|
+
args = parser.parse_args()
|
|
205
|
+
config = json.loads(Path("config.json").read_text())
|
|
206
|
+
token_name = args.github_token_secret or config.get(
|
|
207
|
+
"github_token_secret", "GITHUB_PERSONAL_ACCESS_TOKEN"
|
|
208
|
+
)
|
|
209
|
+
repositories = config.get("repos") or [config["repository"]]
|
|
210
|
+
failures = []
|
|
211
|
+
for repository in repositories:
|
|
212
|
+
options = dict(
|
|
213
|
+
github_token_secret=token_name,
|
|
214
|
+
repository=repository,
|
|
215
|
+
conversation=conversation,
|
|
216
|
+
)
|
|
217
|
+
if dispatcher is not None:
|
|
218
|
+
options["dispatcher"] = dispatcher
|
|
219
|
+
automation = automation_type(**options)
|
|
220
|
+
try:
|
|
221
|
+
automation.run()
|
|
222
|
+
except Exception as exc: # noqa: BLE001 - one repository must not block others
|
|
223
|
+
failures.append(repository)
|
|
224
|
+
print(
|
|
225
|
+
json.dumps({"repository": repository, "error": type(exc).__name__}),
|
|
226
|
+
flush=True,
|
|
227
|
+
)
|
|
228
|
+
if failures:
|
|
229
|
+
raise RuntimeError("Automation failed for: " + ", ".join(failures))
|
|
230
|
+
return str(conversation.id) if conversation else None
|
|
@@ -782,6 +782,9 @@ def _build_implementation_prompt(
|
|
|
782
782
|
branch: str,
|
|
783
783
|
base_branch: str,
|
|
784
784
|
base_sha: str,
|
|
785
|
+
*,
|
|
786
|
+
workspace_instructions: str | None = None,
|
|
787
|
+
github_token_secret: str = "GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
785
788
|
) -> str:
|
|
786
789
|
"""Name the issue and let the agent gather the rest.
|
|
787
790
|
|
|
@@ -794,28 +797,31 @@ def _build_implementation_prompt(
|
|
|
794
797
|
title = issue.get("title", "(no title)").replace('"', "'")
|
|
795
798
|
draft_words = " as a draft" if DRAFT_PULL_REQUEST else " ready for review"
|
|
796
799
|
draft_flag = " --draft" if DRAFT_PULL_REQUEST else ""
|
|
800
|
+
secret_ref = f"${github_token_secret}"
|
|
801
|
+
workspace = workspace_instructions or (
|
|
802
|
+
f"It is a clone of `{base_branch}` at `{base_sha}`, already on branch "
|
|
803
|
+
f"`{branch}`. Do not clone or check out anything else: the code you need is "
|
|
804
|
+
"already here, and the branch is the one the pull request comes from."
|
|
805
|
+
)
|
|
797
806
|
|
|
798
807
|
return (
|
|
799
|
-
"You are an autonomous software engineer. Implement the GitHub issue below
|
|
800
|
-
"the repository already checked out as your working directory.\n\n"
|
|
808
|
+
"You are an autonomous software engineer. Implement the GitHub issue below.\n\n"
|
|
801
809
|
f"Repository : {repo}\n"
|
|
802
810
|
f"Issue : #{number} - \"{title}\"\n"
|
|
803
811
|
f"URL : {issue.get('html_url', '')}\n"
|
|
804
812
|
f"Trigger : latest `{TRIGGER_LABEL}` labeled event {label_event.get('id', '?')} "
|
|
805
813
|
f"at {label_event.get('created_at', '?')}\n\n"
|
|
806
814
|
"Your workspace:\n"
|
|
807
|
-
f"-
|
|
808
|
-
|
|
809
|
-
"
|
|
810
|
-
"- `origin` carries no credential. Every command that talks to GitHub must "
|
|
811
|
-
"name `GITHUB_PERSONAL_ACCESS_TOKEN`, because the value is only put in the "
|
|
815
|
+
f"- {workspace}\n"
|
|
816
|
+
"- Every command that talks to GitHub must explicitly name "
|
|
817
|
+
f"`{github_token_secret}`, because the value is only put in the "
|
|
812
818
|
"environment of a command that mentions it. Never echo it.\n\n"
|
|
813
819
|
"Required workflow:\n"
|
|
814
820
|
"1. Read the issue first. Its title above is all you have been told; fetch the "
|
|
815
821
|
"rest yourself:\n"
|
|
816
822
|
f" `gh issue view {number} --repo {repo} --comments`, or the REST API - "
|
|
817
823
|
f"`/repos/{repo}/issues/{number}` and `/repos/{repo}/issues/{number}/comments` - "
|
|
818
|
-
"
|
|
824
|
+
"using the GitHub credential named above. Never print the token.\n"
|
|
819
825
|
"2. Follow what the issue points at as far as it matters: linked issues and pull "
|
|
820
826
|
"requests, referenced files, failing runs, prior art in the history.\n"
|
|
821
827
|
"3. Read enough of the codebase to place the change where it belongs and to "
|
|
@@ -827,10 +833,10 @@ def _build_implementation_prompt(
|
|
|
827
833
|
"6. Delete scratch files, build output, and virtualenvs the repository does not "
|
|
828
834
|
f"already ignore, then commit everything on `{branch}`.\n"
|
|
829
835
|
"7. Push the branch:\n"
|
|
830
|
-
f" `git push \"https://x-access-token
|
|
836
|
+
f" `git push \"https://x-access-token:{secret_ref}@github.com/"
|
|
831
837
|
f"{repo}.git\" HEAD:refs/heads/{branch}`\n"
|
|
832
838
|
f"8. Open the pull request{draft_words}:\n"
|
|
833
|
-
f" `GH_TOKEN
|
|
839
|
+
f" `GH_TOKEN={secret_ref} gh pr create --repo {repo} "
|
|
834
840
|
f"--base {base_branch} --head {branch}{draft_flag} --title \"[#{number}] {title}\" "
|
|
835
841
|
"--body-file <file>`\n"
|
|
836
842
|
" The body is your pull request description - what changed, why, and what a "
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Select ready GitHub work and delegate each subject to a sandboxed agent."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import main as workflow
|
|
7
|
+
from agent_conversation import AgentConversationDispatcher
|
|
8
|
+
from github_client import GitHubRepository, run_repositories
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IssueToPR(GitHubRepository):
|
|
12
|
+
name = "github-issue-to-pr"
|
|
13
|
+
|
|
14
|
+
def _workspace_instructions(self, branch, revision):
|
|
15
|
+
token = self.token_name
|
|
16
|
+
if revision:
|
|
17
|
+
checkout = f"check out the existing remote branch `{branch}`"
|
|
18
|
+
else:
|
|
19
|
+
checkout = (
|
|
20
|
+
f"create and check out `{branch}` from the repository's base branch"
|
|
21
|
+
)
|
|
22
|
+
return (
|
|
23
|
+
"The workspace starts empty. Clone the repository into it with "
|
|
24
|
+
f"`GH_TOKEN=${{{token}}} gh repo clone {self.repository} .`, then {checkout}. "
|
|
25
|
+
"Keep the remote free of embedded credentials."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def _prompt(self, issue, branch, base_branch, base_sha, *, revision=None):
|
|
29
|
+
review_label = self.config.get("review_label", "openhands-review")
|
|
30
|
+
prompt = workflow._build_implementation_prompt(
|
|
31
|
+
self.repository,
|
|
32
|
+
issue,
|
|
33
|
+
{
|
|
34
|
+
"id": issue.get("updated_at", "?"),
|
|
35
|
+
"created_at": issue.get("updated_at", "?"),
|
|
36
|
+
},
|
|
37
|
+
branch,
|
|
38
|
+
base_branch,
|
|
39
|
+
base_sha,
|
|
40
|
+
workspace_instructions=self._workspace_instructions(branch, revision),
|
|
41
|
+
github_token_secret=self.token_name,
|
|
42
|
+
)
|
|
43
|
+
if revision:
|
|
44
|
+
prompt = (
|
|
45
|
+
f"Revise existing PR #{revision['number']} at exact head "
|
|
46
|
+
f"`{revision['head']['sha']}`. Read its current reviews, inline "
|
|
47
|
+
"comments, discussion, and failing checks directly from GitHub. "
|
|
48
|
+
"Address each current finding or explain with evidence why no code "
|
|
49
|
+
"change is warranted. Push revisions to the existing branch; do not "
|
|
50
|
+
"open another pull request. After responding, remove and reapply the "
|
|
51
|
+
f"`{review_label}` label so the exact new head is reviewed. If no code "
|
|
52
|
+
"change is needed, post that evidence on the PR before reapplying it.\n\n"
|
|
53
|
+
+ prompt
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
prompt += (
|
|
57
|
+
f"\n\nAfter opening the pull request, add the `{review_label}` label so "
|
|
58
|
+
"the independent reviewer checks its exact head."
|
|
59
|
+
)
|
|
60
|
+
return prompt
|
|
61
|
+
|
|
62
|
+
def _submit(
|
|
63
|
+
self, repository_id, issue, branch, base_branch, base_sha, *, revision=None
|
|
64
|
+
):
|
|
65
|
+
revision_sha = revision["head"]["sha"] if revision else None
|
|
66
|
+
key = revision_sha or issue.get("updated_at") or str(issue["number"])
|
|
67
|
+
result = self.dispatcher.deliver(
|
|
68
|
+
subject=f"{repository_id}:issue:{issue['number']}",
|
|
69
|
+
delivery=f"{issue['number']}:{key}",
|
|
70
|
+
prompt=self._prompt(
|
|
71
|
+
issue,
|
|
72
|
+
branch,
|
|
73
|
+
base_branch,
|
|
74
|
+
revision_sha or base_sha,
|
|
75
|
+
revision=revision,
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
print(
|
|
79
|
+
json.dumps(
|
|
80
|
+
{
|
|
81
|
+
"repository": self.repository,
|
|
82
|
+
"issue": issue["number"],
|
|
83
|
+
"pr": revision["number"] if revision else None,
|
|
84
|
+
"disposition": result["disposition"],
|
|
85
|
+
"conversation_id": result["conversation_id"],
|
|
86
|
+
}
|
|
87
|
+
),
|
|
88
|
+
flush=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _try_submit(
|
|
92
|
+
self, repository_id, issue, branch, base_branch, base_sha, *, revision=None
|
|
93
|
+
):
|
|
94
|
+
try:
|
|
95
|
+
self._submit(
|
|
96
|
+
repository_id,
|
|
97
|
+
issue,
|
|
98
|
+
branch,
|
|
99
|
+
base_branch,
|
|
100
|
+
base_sha,
|
|
101
|
+
revision=revision,
|
|
102
|
+
)
|
|
103
|
+
except Exception as exc: # noqa: BLE001 - one issue must not block the scan
|
|
104
|
+
print(
|
|
105
|
+
f"Failed to submit {self.repository} issue "
|
|
106
|
+
f"#{issue.get('number', '?')}: {exc}",
|
|
107
|
+
flush=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def run(self):
|
|
111
|
+
trigger_label = self.config.get("trigger_label", workflow.TRIGGER_LABEL)
|
|
112
|
+
branch_prefix = self.config.get("branch_prefix", workflow.BRANCH_PREFIX)
|
|
113
|
+
repository = self.gh("GET", "")
|
|
114
|
+
repository_id = repository["id"]
|
|
115
|
+
base_branch = self.config.get("base_branch") or repository["default_branch"]
|
|
116
|
+
base_sha = self.gh("GET", f"/git/ref/heads/{base_branch}")["object"]["sha"]
|
|
117
|
+
issues = {issue["number"]: issue for issue in self.open_issues()}
|
|
118
|
+
|
|
119
|
+
open_prs = self.gh_pages("/pulls?state=open")
|
|
120
|
+
issue_prs = {}
|
|
121
|
+
for pr in open_prs:
|
|
122
|
+
match = re.fullmatch(
|
|
123
|
+
re.escape(branch_prefix) + r"-(\d+)", pr["head"]["ref"]
|
|
124
|
+
)
|
|
125
|
+
if match:
|
|
126
|
+
issue_prs[int(match[1])] = pr
|
|
127
|
+
|
|
128
|
+
for issue_number, pr in sorted(issue_prs.items()):
|
|
129
|
+
issue = issues.get(issue_number)
|
|
130
|
+
if issue is None:
|
|
131
|
+
continue
|
|
132
|
+
if self.statuses(pr["head"]["sha"]).get("software-factory/review") not in {
|
|
133
|
+
"failure",
|
|
134
|
+
"error",
|
|
135
|
+
}:
|
|
136
|
+
continue
|
|
137
|
+
self._try_submit(
|
|
138
|
+
repository_id,
|
|
139
|
+
issue,
|
|
140
|
+
pr["head"]["ref"],
|
|
141
|
+
(pr.get("base") or {}).get("ref") or base_branch,
|
|
142
|
+
base_sha,
|
|
143
|
+
revision=pr,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
ready = [
|
|
147
|
+
issue
|
|
148
|
+
for issue in issues.values()
|
|
149
|
+
if issue["number"] not in issue_prs
|
|
150
|
+
and trigger_label in {label["name"] for label in issue.get("labels", [])}
|
|
151
|
+
and self.dependencies_complete(issue)
|
|
152
|
+
]
|
|
153
|
+
for issue in sorted(
|
|
154
|
+
ready,
|
|
155
|
+
key=lambda item: (
|
|
156
|
+
"priority:high"
|
|
157
|
+
not in {label["name"] for label in item.get("labels", [])},
|
|
158
|
+
item["number"],
|
|
159
|
+
),
|
|
160
|
+
):
|
|
161
|
+
self._try_submit(
|
|
162
|
+
repository_id,
|
|
163
|
+
issue,
|
|
164
|
+
f"{branch_prefix}-{issue['number']}",
|
|
165
|
+
base_branch,
|
|
166
|
+
base_sha,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
with AgentConversationDispatcher() as dispatcher:
|
|
172
|
+
run_repositories(IssueToPR, dispatcher=dispatcher)
|
|
@@ -9,29 +9,26 @@ triggers:
|
|
|
9
9
|
|
|
10
10
|
Prioritize open issues and establish acceptance criteria before marking them ready for development.
|
|
11
11
|
|
|
12
|
-
Create this automation separately from implementation and review.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Use a fine-grained GitHub PAT limited to the selected repositories with
|
|
16
|
-
Issues: read and write. Store it in the Agent Server secret store and select its name in
|
|
17
|
-
the profile. Never put the token value in the automation definition or prompt.
|
|
12
|
+
Create this automation separately from implementation and review. Use a
|
|
13
|
+
fine-grained GitHub PAT limited to the selected repositories with Issues: read and
|
|
14
|
+
write. Never put the token value in the automation definition or prompt.
|
|
18
15
|
|
|
19
|
-
Package
|
|
20
|
-
`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
another secret name, pass that name instead. Naming a secret does not grant it:
|
|
25
|
-
the Agent Server only supplies secrets allowed by the profile.
|
|
16
|
+
Package `worker.py` with the shared `github_client.py` and
|
|
17
|
+
`agent_conversation.py` helpers. Supply `config.json` with `repos` and the saved
|
|
18
|
+
GitHub secret name; the entrypoint is `python3 worker.py`. Include that secret in
|
|
19
|
+
the selected profile so the delegated agent can use GitHub. Naming it in the
|
|
20
|
+
automation does not grant it to the agent.
|
|
26
21
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
The scanner is an ordinary Automation host command. It submits selected issues
|
|
23
|
+
through the shared KV-backed conversation dispatcher, which creates or resumes
|
|
24
|
+
one stable conversation per issue through the Software Agent SDK. Only that
|
|
25
|
+
agent workspace is local or Docker. Automation owns scheduling, cancellation,
|
|
26
|
+
and cleanup.
|
|
30
27
|
|
|
31
28
|
Honor `Depends on: #12, #13` lines. A dependency must be closed as completed.
|
|
32
29
|
Post readable acceptance criteria and rationale. Add `ready-for-dev` only when
|
|
33
30
|
criteria are actionable; preserve existing issue labels. Unclear issues stay open
|
|
34
31
|
for clarification. Do not implement code or accept pull requests.
|
|
35
32
|
|
|
36
|
-
Each scheduled run
|
|
37
|
-
|
|
33
|
+
Each scheduled run submits every changed eligible issue. A failure on one issue is
|
|
34
|
+
reported and does not prevent the remaining issues from being submitted.
|