@openhands/extensions 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +881 -0
- package/.codex-plugin/marketplace.json +881 -0
- package/.github/workflows/check-extensions.yml +4 -0
- package/.github/workflows/npm-publish.yml +9 -3
- package/.github/workflows/pr-artifacts.yml +294 -0
- package/.github/workflows/pr-description-check.yml +4 -2
- package/.plugin/marketplace.json +881 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +27 -3
- package/README.md +6 -2
- package/automations/bundle-index.js +22 -3
- package/automations/catalog/github-agents-md-maintainer/manifest.json +2 -1
- package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
- package/automations/catalog/github-issue-to-pr/manifest.json +24 -15
- package/automations/catalog/github-issue-triage/manifest.json +73 -0
- package/automations/catalog/github-pr-reviewer/manifest.json +23 -15
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +124 -0
- package/automations/catalog-index.js +17 -11
- package/integrations/catalog/reportportal.json +76 -0
- package/integrations/catalog-index.js +6 -4
- package/marketplaces/openhands-extensions.json +42 -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 +143 -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 +2 -2
- 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 +143 -0
- package/skills/github/scripts/github_client.py +230 -0
- 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/SKILL.md +7 -1
- package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
- package/skills/github-agents-md-maintainer/scripts/main.py +4 -40
- 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 +15 -1
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +143 -0
- package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
- package/skills/github-issue-to-pr/scripts/main.py +19 -50
- 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/.plugin/plugin.json +9 -0
- package/skills/github-issue-triage/README.md +9 -0
- package/skills/github-issue-triage/SKILL.md +34 -0
- package/skills/github-issue-triage/commands/github-issue-triage.md +8 -0
- package/skills/github-issue-triage/scripts/agent_conversation.py +143 -0
- package/skills/github-issue-triage/scripts/github_client.py +230 -0
- package/skills/github-issue-triage/scripts/worker.py +106 -0
- 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 +15 -1
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +143 -0
- package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
- package/skills/github-pr-reviewer/scripts/main.py +23 -49
- 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/SKILL.md +7 -1
- package/skills/github-repo-monitor/scripts/github_client.py +230 -0
- package/skills/github-repo-monitor/scripts/main.py +4 -46
- 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/gitlab-issue-to-mr/.plugin/plugin.json +19 -0
- package/skills/gitlab-issue-to-mr/README.md +92 -0
- package/skills/gitlab-issue-to-mr/SKILL.md +408 -0
- package/skills/gitlab-issue-to-mr/commands/issue-to-mr-setup.md +8 -0
- package/skills/gitlab-issue-to-mr/references/state-schema.md +140 -0
- package/skills/gitlab-issue-to-mr/scripts/main.py +1448 -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 +52 -6
- 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-automation/README.md +1 -1
- package/skills/openhands-automation/SKILL.md +18 -8
- 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-enterprise-troubleshooting/.plugin/plugin.json +20 -0
- package/skills/openhands-enterprise-troubleshooting/README.md +79 -0
- package/skills/openhands-enterprise-troubleshooting/SKILL.md +339 -0
- package/skills/openhands-enterprise-troubleshooting/references/diagnostics.md +698 -0
- package/skills/openhands-enterprise-troubleshooting/references/support-bundle-analysis.md +570 -0
- package/skills/openhands-enterprise-troubleshooting/scripts/bundle_triage.py +532 -0
- package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
- package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
- package/skills/openhands-sdk/SKILL.md +2 -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 +31 -0
- package/tests/test_agent_conversation_dispatch.py +180 -0
- package/tests/test_automation_setup.py +3 -3
- package/tests/test_github_automation_foundation.py +73 -0
- package/tests/test_github_developer_delivery.py +150 -0
- package/tests/test_github_reviewer_delivery.py +110 -0
- package/tests/test_github_skill_installation.py +63 -0
- package/tests/test_github_triage_delivery.py +90 -0
- package/tests/test_github_watchdog_delivery.py +233 -0
- package/tests/test_gitlab_issue_to_mr.py +823 -0
- package/.pr/custom-automation-event-trigger.png +0 -0
- package/.pr/custom-automation-plugin-fields.png +0 -0
- package/.pr/custom-automation-prompt-review.png +0 -0
- package/.pr/custom-automation-prompt-scheduled.png +0 -0
- package/.pr/custom-automation-upload-fields.png +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Follow delivery progress and merge only independently accepted, current PRs."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from github_client import GitHubRepository, run_repositories
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeliveryWatchdog(GitHubRepository):
|
|
10
|
+
name = "github-delivery-watchdog"
|
|
11
|
+
|
|
12
|
+
def ci_passed(self, sha):
|
|
13
|
+
"""Actions read works with fine-grained PATs; permission errors fail closed."""
|
|
14
|
+
runs = {}
|
|
15
|
+
for page in range(1, 11):
|
|
16
|
+
result = self.gh(
|
|
17
|
+
"GET", f"/actions/runs?head_sha={sha}&per_page=100&page={page}"
|
|
18
|
+
)
|
|
19
|
+
for run in result["workflow_runs"]:
|
|
20
|
+
if run["head_sha"] != sha:
|
|
21
|
+
return False
|
|
22
|
+
runs[run["id"]] = run
|
|
23
|
+
if len(result["workflow_runs"]) < 100:
|
|
24
|
+
if result["total_count"] != len(runs):
|
|
25
|
+
return False
|
|
26
|
+
latest = {}
|
|
27
|
+
for run in sorted(
|
|
28
|
+
runs.values(), key=lambda r: r["run_number"], reverse=True
|
|
29
|
+
):
|
|
30
|
+
# A newer execution supersedes the same workflow trigger,
|
|
31
|
+
# but a passing push must not hide a failing PR workflow.
|
|
32
|
+
latest.setdefault(
|
|
33
|
+
(run["workflow_id"], run["event"], run["head_branch"]), run
|
|
34
|
+
)
|
|
35
|
+
return set(self.config.get("required_workflow_ids", [])) <= {
|
|
36
|
+
r["workflow_id"] for r in latest.values()
|
|
37
|
+
} and all(
|
|
38
|
+
r["status"] == "completed" and r["conclusion"] == "success"
|
|
39
|
+
for r in latest.values()
|
|
40
|
+
)
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
def merge(self, number, sha):
|
|
44
|
+
pr = self.gh("GET", f"/pulls/{number}")
|
|
45
|
+
prefix = re.escape(self.config.get("branch_prefix", "openhands/issue"))
|
|
46
|
+
if not (
|
|
47
|
+
pr["state"] == "open"
|
|
48
|
+
and pr["head"]["sha"] == sha
|
|
49
|
+
and pr["base"]["ref"] == self.base_branch
|
|
50
|
+
and re.fullmatch(prefix + r"-\d+", pr["head"]["ref"])
|
|
51
|
+
and not pr["draft"]
|
|
52
|
+
and pr["mergeable"] is True
|
|
53
|
+
):
|
|
54
|
+
return None
|
|
55
|
+
statuses = self.statuses(sha)
|
|
56
|
+
required = ("software-factory/tests", "software-factory/review")
|
|
57
|
+
if not all(statuses.get(c) == "success" for c in required):
|
|
58
|
+
return None
|
|
59
|
+
if not all(
|
|
60
|
+
value == "success" for value in statuses.values()
|
|
61
|
+
) or not self.ci_passed(sha):
|
|
62
|
+
return None
|
|
63
|
+
comparison = self.gh("GET", f"/compare/{pr['base']['sha']}...{sha}")
|
|
64
|
+
if comparison["status"] in ("behind", "diverged") and comparison.get(
|
|
65
|
+
"behind_by", 0
|
|
66
|
+
):
|
|
67
|
+
self.gh(
|
|
68
|
+
"POST",
|
|
69
|
+
f"/issues/{number}/labels",
|
|
70
|
+
{"labels": [self.config.get("trigger_label", "openhands-review")]},
|
|
71
|
+
)
|
|
72
|
+
return self.gh(
|
|
73
|
+
"PUT", f"/pulls/{number}/update-branch", {"expected_head_sha": sha}
|
|
74
|
+
)
|
|
75
|
+
if comparison["status"] not in ("ahead", "identical"):
|
|
76
|
+
return None
|
|
77
|
+
return self.gh(
|
|
78
|
+
"PUT", f"/pulls/{number}/merge", {"sha": sha, "merge_method": "squash"}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def run(self):
|
|
82
|
+
for pr in self.gh_pages("/pulls?state=open"):
|
|
83
|
+
try:
|
|
84
|
+
result = self.merge(pr["number"], pr["head"]["sha"])
|
|
85
|
+
except Exception as exc: # noqa: BLE001 - one PR must not block other repositories
|
|
86
|
+
# A conflict, permission failure, or unavailable API cannot
|
|
87
|
+
# authorize a merge or prevent checking unrelated pull requests.
|
|
88
|
+
result = {
|
|
89
|
+
"error": type(exc).__name__,
|
|
90
|
+
"status": getattr(exc, "code", None),
|
|
91
|
+
}
|
|
92
|
+
print(json.dumps({"pr": pr["number"], "merge": result}), flush=True)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
run_repositories(DeliveryWatchdog)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "github-issue-to-pr",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create an automation that implements GitHub issues when a configurable trigger label is applied. It clones the default branch, starts one OpenHands conversation per label event, then commits, pushes, and opens the pull request itself, without ever handing the agent a GitHub credential.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "OpenHands",
|
|
7
|
+
"email": "contact@all-hands.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/OpenHands/extensions",
|
|
10
|
+
"repository": "https://github.com/OpenHands/extensions",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"github",
|
|
14
|
+
"issue",
|
|
15
|
+
"pull-request",
|
|
16
|
+
"automation",
|
|
17
|
+
"integration"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "github-issue-to-pr",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create an automation that implements GitHub issues when a configurable trigger label is applied. It clones the default branch, starts one OpenHands conversation per label event, then commits, pushes, and opens the pull request itself, without ever handing the agent a GitHub credential.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "OpenHands",
|
|
7
|
+
"email": "contact@all-hands.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/OpenHands/extensions",
|
|
10
|
+
"repository": "https://github.com/OpenHands/extensions",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"github",
|
|
14
|
+
"issue",
|
|
15
|
+
"pull-request",
|
|
16
|
+
"automation",
|
|
17
|
+
"integration"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -11,6 +11,13 @@ triggers:
|
|
|
11
11
|
|
|
12
12
|
# GitHub Issue to PR Automation
|
|
13
13
|
|
|
14
|
+
## Agent Canvas catalog
|
|
15
|
+
|
|
16
|
+
For new Agent Canvas installations, use the **GitHub issue to PR** catalog
|
|
17
|
+
entry. Its deterministic `worker.py` scanner delegates each eligible issue to a
|
|
18
|
+
stable conversation using the selected agent profile. The manual upload flow
|
|
19
|
+
below remains for existing deployments and is deprecated for new installations.
|
|
20
|
+
|
|
14
21
|
Create a cron automation that watches one or more GitHub repositories for issues
|
|
15
22
|
with a trigger label, starts an OpenHands conversation once per label event with
|
|
16
23
|
the repository's default branch already checked out, and opens a pull request
|
|
@@ -40,6 +47,11 @@ environment of a command that mentions it, and masks it in the output.
|
|
|
40
47
|
|
|
41
48
|
---
|
|
42
49
|
|
|
50
|
+
The script imports shared GitHub transport from
|
|
51
|
+
`scripts/github_client.py`, installed with this skill. Include it beside
|
|
52
|
+
`main.py` when packaging manually, as shown below; catalog bundles include it
|
|
53
|
+
automatically.
|
|
54
|
+
|
|
43
55
|
## Prerequisites
|
|
44
56
|
|
|
45
57
|
### Required secret
|
|
@@ -205,9 +217,11 @@ Use a safe string writer such as `json.dumps(value)` when inserting user-provide
|
|
|
205
217
|
repository names, labels, or prefixes into Python string literals.
|
|
206
218
|
`json.dumps(list_of_repos)` produces the whole `REPOS` list safely in one step.
|
|
207
219
|
|
|
208
|
-
|
|
220
|
+
Run these commands from this skill's directory and write the customized script
|
|
221
|
+
to a temporary build directory:
|
|
209
222
|
```bash
|
|
210
223
|
mkdir -p /tmp/issue-to-pr-build
|
|
224
|
+
cp -L scripts/github_client.py /tmp/issue-to-pr-build/github_client.py
|
|
211
225
|
# write the customized main.py to /tmp/issue-to-pr-build/main.py
|
|
212
226
|
```
|
|
213
227
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Idempotently deliver automation work to profile-backed conversations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from urllib.error import HTTPError
|
|
6
|
+
from urllib.request import Request, urlopen
|
|
7
|
+
from uuid import NAMESPACE_URL, UUID, uuid5
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from openhands.sdk import RemoteConversation
|
|
11
|
+
from openhands.sdk.conversation.request import (
|
|
12
|
+
SendMessageRequest,
|
|
13
|
+
StartConversationRequest,
|
|
14
|
+
)
|
|
15
|
+
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
16
|
+
from openhands.sdk.llm.message import TextContent
|
|
17
|
+
from openhands.sdk.workspace import LocalWorkspace, RemoteWorkspace
|
|
18
|
+
|
|
19
|
+
_CONVERSATION_KEY_PREFIX = "agent-conversation-"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _register_tools() -> None:
|
|
23
|
+
"""Register tool models needed to deserialize an attached agent."""
|
|
24
|
+
from openhands.tools import register_default_tools
|
|
25
|
+
|
|
26
|
+
register_default_tools()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _kv_request(key: str, method: str, value: dict | None = None) -> dict | None:
|
|
30
|
+
base_url = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
|
|
31
|
+
token = os.environ.get("AUTOMATION_KV_TOKEN", "")
|
|
32
|
+
if not base_url or not token:
|
|
33
|
+
raise RuntimeError("Automation KV is required for agent conversation dispatch")
|
|
34
|
+
request = Request(
|
|
35
|
+
f"{base_url}/v1/kv/{key}",
|
|
36
|
+
data=json.dumps(value).encode() if value is not None else None,
|
|
37
|
+
headers={
|
|
38
|
+
"Authorization": f"Bearer {token}",
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
},
|
|
41
|
+
method=method,
|
|
42
|
+
)
|
|
43
|
+
try:
|
|
44
|
+
with urlopen(request, timeout=90) as response:
|
|
45
|
+
body = json.load(response)
|
|
46
|
+
except HTTPError as exc:
|
|
47
|
+
if method == "GET" and exc.code == 404:
|
|
48
|
+
return None
|
|
49
|
+
raise
|
|
50
|
+
return body.get("value") if method == "GET" else body
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AgentConversationDispatcher:
|
|
54
|
+
"""Deliver one revision at a time to a stable conversation for each subject."""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self.agent_url = os.environ["AGENT_SERVER_URL"]
|
|
58
|
+
self.api_key = os.environ["SESSION_API_KEY"]
|
|
59
|
+
self.profile_id = UUID(os.environ["AUTOMATION_AGENT_PROFILE_ID"])
|
|
60
|
+
payload = json.loads(os.environ["AUTOMATION_EVENT_PAYLOAD"])
|
|
61
|
+
self.automation_id = str(payload["automation_id"])
|
|
62
|
+
self._workspace: RemoteWorkspace | None = None
|
|
63
|
+
|
|
64
|
+
def __enter__(self):
|
|
65
|
+
_register_tools()
|
|
66
|
+
self._workspace = RemoteWorkspace(
|
|
67
|
+
host=self.agent_url,
|
|
68
|
+
api_key=self.api_key,
|
|
69
|
+
working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
|
|
70
|
+
)
|
|
71
|
+
self._workspace.__enter__()
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, *args):
|
|
75
|
+
assert self._workspace is not None
|
|
76
|
+
return self._workspace.__exit__(*args)
|
|
77
|
+
|
|
78
|
+
def deliver(self, subject: str, delivery: str, prompt: str) -> dict[str, str]:
|
|
79
|
+
conversation_id = uuid5(NAMESPACE_URL, f"{self.automation_id}:{subject}")
|
|
80
|
+
state_key = f"{_CONVERSATION_KEY_PREFIX}{conversation_id}"
|
|
81
|
+
record = _kv_request(state_key, "GET") or {}
|
|
82
|
+
if self._workspace is None:
|
|
83
|
+
raise RuntimeError("AgentConversationDispatcher must be used as a context")
|
|
84
|
+
|
|
85
|
+
same_delivery = record.get("delivery") == delivery
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
conversation = RemoteConversation.attach(
|
|
89
|
+
self._workspace, conversation_id, visualizer=None
|
|
90
|
+
)
|
|
91
|
+
disposition = "resumed"
|
|
92
|
+
except httpx.HTTPStatusError as exc:
|
|
93
|
+
if exc.response.status_code != 404:
|
|
94
|
+
raise
|
|
95
|
+
conversation = RemoteConversation.create(
|
|
96
|
+
self._workspace,
|
|
97
|
+
StartConversationRequest(
|
|
98
|
+
workspace=LocalWorkspace(working_dir="/workspace"),
|
|
99
|
+
conversation_id=conversation_id,
|
|
100
|
+
agent_profile_id=self.profile_id,
|
|
101
|
+
initial_message=SendMessageRequest(
|
|
102
|
+
content=[TextContent(text=prompt)], run=True
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
visualizer=None,
|
|
106
|
+
)
|
|
107
|
+
disposition = "created"
|
|
108
|
+
try:
|
|
109
|
+
if disposition == "resumed":
|
|
110
|
+
if same_delivery:
|
|
111
|
+
if conversation.state.execution_status in (
|
|
112
|
+
ConversationExecutionStatus.IDLE,
|
|
113
|
+
ConversationExecutionStatus.RUNNING,
|
|
114
|
+
ConversationExecutionStatus.PAUSED,
|
|
115
|
+
):
|
|
116
|
+
conversation.run(blocking=False)
|
|
117
|
+
else:
|
|
118
|
+
disposition = "deduplicated"
|
|
119
|
+
else:
|
|
120
|
+
conversation.send_message(prompt)
|
|
121
|
+
conversation.run(blocking=False)
|
|
122
|
+
finally:
|
|
123
|
+
conversation.close()
|
|
124
|
+
|
|
125
|
+
if disposition == "deduplicated":
|
|
126
|
+
return {
|
|
127
|
+
"disposition": disposition,
|
|
128
|
+
"conversation_id": str(conversation_id),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_kv_request(
|
|
132
|
+
state_key,
|
|
133
|
+
"PUT",
|
|
134
|
+
{
|
|
135
|
+
"subject": subject,
|
|
136
|
+
"conversation_id": str(conversation_id),
|
|
137
|
+
"delivery": delivery,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
return {
|
|
141
|
+
"disposition": disposition,
|
|
142
|
+
"conversation_id": str(conversation_id),
|
|
143
|
+
}
|
|
@@ -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
|
|
@@ -33,7 +33,9 @@ import urllib.error
|
|
|
33
33
|
import urllib.request
|
|
34
34
|
from collections.abc import Callable
|
|
35
35
|
from pathlib import Path
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
from github_client import github_request as _github_request
|
|
38
|
+
from github_client import github_paginate as _github_paginate
|
|
37
39
|
|
|
38
40
|
# Configuration. Two setup paths write it, and both end up here:
|
|
39
41
|
#
|
|
@@ -350,45 +352,6 @@ def save_state(repo: str, state: dict) -> None:
|
|
|
350
352
|
# ── GitHub REST ───────────────────────────────────────────────────────────────
|
|
351
353
|
|
|
352
354
|
|
|
353
|
-
def _github_request(
|
|
354
|
-
token: str,
|
|
355
|
-
method: str,
|
|
356
|
-
path: str,
|
|
357
|
-
params: dict | None = None,
|
|
358
|
-
body: dict | None = None,
|
|
359
|
-
) -> tuple:
|
|
360
|
-
url = f"https://api.github.com{path}"
|
|
361
|
-
if params:
|
|
362
|
-
url = f"{url}?{urlencode(params)}"
|
|
363
|
-
headers = {
|
|
364
|
-
"Authorization": f"Bearer {token}",
|
|
365
|
-
"Accept": "application/vnd.github+json",
|
|
366
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
367
|
-
"Content-Type": "application/json",
|
|
368
|
-
}
|
|
369
|
-
data = json.dumps(body).encode() if body is not None else None
|
|
370
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
371
|
-
with urllib.request.urlopen(req) as r:
|
|
372
|
-
raw = r.read()
|
|
373
|
-
return (json.loads(raw) if raw.strip() else {}), dict(r.headers)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def _github_paginate(token: str, path: str, params: dict | None = None) -> list:
|
|
377
|
-
results = []
|
|
378
|
-
page = 1
|
|
379
|
-
base_params = dict(params or {})
|
|
380
|
-
base_params.setdefault("per_page", 100)
|
|
381
|
-
while True:
|
|
382
|
-
base_params["page"] = page
|
|
383
|
-
data, _ = _github_request(token, "GET", path, params=base_params)
|
|
384
|
-
if not isinstance(data, list):
|
|
385
|
-
break
|
|
386
|
-
results.extend(data)
|
|
387
|
-
if len(data) < base_params["per_page"]:
|
|
388
|
-
break
|
|
389
|
-
page += 1
|
|
390
|
-
return results
|
|
391
|
-
|
|
392
355
|
|
|
393
356
|
def _resolve_github_token() -> str:
|
|
394
357
|
try:
|
|
@@ -819,6 +782,9 @@ def _build_implementation_prompt(
|
|
|
819
782
|
branch: str,
|
|
820
783
|
base_branch: str,
|
|
821
784
|
base_sha: str,
|
|
785
|
+
*,
|
|
786
|
+
workspace_instructions: str | None = None,
|
|
787
|
+
github_token_secret: str = "GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
822
788
|
) -> str:
|
|
823
789
|
"""Name the issue and let the agent gather the rest.
|
|
824
790
|
|
|
@@ -831,28 +797,31 @@ def _build_implementation_prompt(
|
|
|
831
797
|
title = issue.get("title", "(no title)").replace('"', "'")
|
|
832
798
|
draft_words = " as a draft" if DRAFT_PULL_REQUEST else " ready for review"
|
|
833
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
|
+
)
|
|
834
806
|
|
|
835
807
|
return (
|
|
836
|
-
"You are an autonomous software engineer. Implement the GitHub issue below
|
|
837
|
-
"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"
|
|
838
809
|
f"Repository : {repo}\n"
|
|
839
810
|
f"Issue : #{number} - \"{title}\"\n"
|
|
840
811
|
f"URL : {issue.get('html_url', '')}\n"
|
|
841
812
|
f"Trigger : latest `{TRIGGER_LABEL}` labeled event {label_event.get('id', '?')} "
|
|
842
813
|
f"at {label_event.get('created_at', '?')}\n\n"
|
|
843
814
|
"Your workspace:\n"
|
|
844
|
-
f"-
|
|
845
|
-
|
|
846
|
-
"
|
|
847
|
-
"- `origin` carries no credential. Every command that talks to GitHub must "
|
|
848
|
-
"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 "
|
|
849
818
|
"environment of a command that mentions it. Never echo it.\n\n"
|
|
850
819
|
"Required workflow:\n"
|
|
851
820
|
"1. Read the issue first. Its title above is all you have been told; fetch the "
|
|
852
821
|
"rest yourself:\n"
|
|
853
822
|
f" `gh issue view {number} --repo {repo} --comments`, or the REST API - "
|
|
854
823
|
f"`/repos/{repo}/issues/{number}` and `/repos/{repo}/issues/{number}/comments` - "
|
|
855
|
-
"
|
|
824
|
+
"using the GitHub credential named above. Never print the token.\n"
|
|
856
825
|
"2. Follow what the issue points at as far as it matters: linked issues and pull "
|
|
857
826
|
"requests, referenced files, failing runs, prior art in the history.\n"
|
|
858
827
|
"3. Read enough of the codebase to place the change where it belongs and to "
|
|
@@ -864,10 +833,10 @@ def _build_implementation_prompt(
|
|
|
864
833
|
"6. Delete scratch files, build output, and virtualenvs the repository does not "
|
|
865
834
|
f"already ignore, then commit everything on `{branch}`.\n"
|
|
866
835
|
"7. Push the branch:\n"
|
|
867
|
-
f" `git push \"https://x-access-token
|
|
836
|
+
f" `git push \"https://x-access-token:{secret_ref}@github.com/"
|
|
868
837
|
f"{repo}.git\" HEAD:refs/heads/{branch}`\n"
|
|
869
838
|
f"8. Open the pull request{draft_words}:\n"
|
|
870
|
-
f" `GH_TOKEN
|
|
839
|
+
f" `GH_TOKEN={secret_ref} gh pr create --repo {repo} "
|
|
871
840
|
f"--base {base_branch} --head {branch}{draft_flag} --title \"[#{number}] {title}\" "
|
|
872
841
|
"--body-file <file>`\n"
|
|
873
842
|
" The body is your pull request description - what changed, why, and what a "
|