@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,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
|
|
@@ -30,7 +30,10 @@ import urllib.error
|
|
|
30
30
|
import urllib.request
|
|
31
31
|
from collections.abc import Callable
|
|
32
32
|
from pathlib import Path
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
from github_client import github_request as _github_request
|
|
35
|
+
from github_client import github_paginate as _github_paginate
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
# Configuration. Two setup paths write it, and both end up here:
|
|
36
39
|
#
|
|
@@ -341,45 +344,6 @@ def save_state(repo: str, state: dict) -> None:
|
|
|
341
344
|
# ── GitHub REST ───────────────────────────────────────────────────────────────
|
|
342
345
|
|
|
343
346
|
|
|
344
|
-
def _github_request(
|
|
345
|
-
token: str,
|
|
346
|
-
method: str,
|
|
347
|
-
path: str,
|
|
348
|
-
params: dict | None = None,
|
|
349
|
-
body: dict | None = None,
|
|
350
|
-
) -> tuple:
|
|
351
|
-
url = f"https://api.github.com{path}"
|
|
352
|
-
if params:
|
|
353
|
-
url = f"{url}?{urlencode(params)}"
|
|
354
|
-
headers = {
|
|
355
|
-
"Authorization": f"Bearer {token}",
|
|
356
|
-
"Accept": "application/vnd.github+json",
|
|
357
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
358
|
-
"Content-Type": "application/json",
|
|
359
|
-
}
|
|
360
|
-
data = json.dumps(body).encode() if body is not None else None
|
|
361
|
-
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
362
|
-
with urllib.request.urlopen(req) as r:
|
|
363
|
-
raw = r.read()
|
|
364
|
-
return (json.loads(raw) if raw.strip() else {}), dict(r.headers)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def _github_paginate(token: str, path: str, params: dict | None = None) -> list:
|
|
368
|
-
results = []
|
|
369
|
-
page = 1
|
|
370
|
-
base_params = dict(params or {})
|
|
371
|
-
base_params.setdefault("per_page", 100)
|
|
372
|
-
while True:
|
|
373
|
-
base_params["page"] = page
|
|
374
|
-
data, _ = _github_request(token, "GET", path, params=base_params)
|
|
375
|
-
if not isinstance(data, list):
|
|
376
|
-
break
|
|
377
|
-
results.extend(data)
|
|
378
|
-
if len(data) < base_params["per_page"]:
|
|
379
|
-
break
|
|
380
|
-
page += 1
|
|
381
|
-
return results
|
|
382
|
-
|
|
383
347
|
|
|
384
348
|
def _resolve_github_token() -> str:
|
|
385
349
|
try:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# GitHub delivery watchdog
|
|
2
|
+
|
|
3
|
+
Periodically check pull requests and merge only current heads with independent review, tests, and passing CI.
|
|
4
|
+
|
|
5
|
+
Run this independently from issue triage, implementation, and review. Configure
|
|
6
|
+
a repository-scoped fine-grained PAT by its saved secret name. The watchdog uses
|
|
7
|
+
Actions and commit statuses and sends a conditional merge request for the exact
|
|
8
|
+
accepted head. It does not create an agent or change application code.
|
|
9
|
+
|
|
10
|
+
See [SKILL.md](SKILL.md) for permissions, acceptance gates, and configuration.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github-delivery-watchdog
|
|
3
|
+
description: Periodically check pull requests and merge only current heads with independent review, tests, and passing CI.
|
|
4
|
+
triggers:
|
|
5
|
+
- /github-delivery-watchdog
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GitHub delivery watchdog
|
|
9
|
+
|
|
10
|
+
This is a deterministic scheduled host command. It creates no agent or
|
|
11
|
+
conversation and needs no agent profile. Configure a repository-scoped
|
|
12
|
+
fine-grained PAT with Contents and Issues read/write plus Pull requests, Actions,
|
|
13
|
+
Commit statuses, and Metadata read. Contents write permits merge; Issues write
|
|
14
|
+
retains the review label when the branch is updated. Never put the token value in
|
|
15
|
+
the automation definition.
|
|
16
|
+
|
|
17
|
+
Package `scripts/worker.py` as `worker.py` and the shared
|
|
18
|
+
`scripts/github_client.py` as `github_client.py`.
|
|
19
|
+
The catalog bundle declares these exact files. Its `config.json` supplies
|
|
20
|
+
`repos`, `branch_prefix`, and the saved secret name. The shared GitHub client
|
|
21
|
+
resolves only that named secret. Automation owns scheduling and cancellation.
|
|
22
|
+
|
|
23
|
+
Set `branch_prefix` (default `openhands/issue`), `base_branch` (defaults to the repository's default branch),
|
|
24
|
+
and `required_workflow_ids` when particular Actions workflows must run. The
|
|
25
|
+
watchdog requires `software-factory/tests` and `software-factory/review` success
|
|
26
|
+
statuses on the exact head, all other statuses and Actions passing, a current
|
|
27
|
+
base, a non-draft PR, and GitHub reporting it mergeable. Missing, pending, failed,
|
|
28
|
+
or inaccessible evidence does not permit merge. A changed head requires fresh
|
|
29
|
+
review and tests. When an accepted branch is behind the base, the watchdog asks
|
|
30
|
+
GitHub to update it and retains the review trigger label; it considers the new
|
|
31
|
+
head only on a later run. The merge request includes the expected head SHA.
|
|
32
|
+
|
|
33
|
+
Actions are optional when `required_workflow_ids` is empty; the two acceptance
|
|
34
|
+
statuses remain mandatory. Configure workflow IDs when GitHub Actions must also
|
|
35
|
+
supply evidence. Branch protection remains GitHub's final merge gate.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
# auto-generated by sync_extensions.py
|
|
3
|
+
description: Periodically check pull requests and merge only current heads with independent review, tests, and passing CI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
|
|
7
|
+
|
|
8
|
+
$ARGUMENTS
|
|
@@ -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
|