@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,92 @@
|
|
|
1
|
+
# GitLab Issue to MR
|
|
2
|
+
|
|
3
|
+
Create an automation that implements GitLab issues when a configurable trigger
|
|
4
|
+
label is applied, and opens the merge request for you.
|
|
5
|
+
|
|
6
|
+
## Trigger
|
|
7
|
+
|
|
8
|
+
This skill is activated by:
|
|
9
|
+
|
|
10
|
+
- `/issue-to-mr:setup`
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Implements an issue on demand by watching for a GitLab label event
|
|
15
|
+
- Watches several projects from a single automation, each with its own state
|
|
16
|
+
- Works against gitlab.com and self-managed instances, subgroups included
|
|
17
|
+
- Processes each label application exactly once, with persistent state
|
|
18
|
+
- Re-runs on demand by removing and re-applying the label, on a fresh branch
|
|
19
|
+
- Clones the default branch for the agent, and removes the clone when the task
|
|
20
|
+
ends, so nothing accumulates between runs
|
|
21
|
+
- Lets the agent open the merge request, and opens it in Python when the agent
|
|
22
|
+
did not
|
|
23
|
+
- Opens draft merge requests by default, titled `Draft: [#42] <issue title>`,
|
|
24
|
+
with the agent's summary and `Closes #42` in the description
|
|
25
|
+
- Comments on the issue when work starts, when it finishes, and when it does not
|
|
26
|
+
- Posts the agent's answer instead of a merge request when it made no changes
|
|
27
|
+
- Caps how many conversations one poll starts, so a labelled backlog does not
|
|
28
|
+
start dozens at once
|
|
29
|
+
|
|
30
|
+
## What the agent is told, and what it can reach
|
|
31
|
+
|
|
32
|
+
The prompt names the project, the issue IID and title, and its URL. The agent
|
|
33
|
+
fetches the description, the discussion, and anything they link to itself - a
|
|
34
|
+
copy pasted at dispatch would already be stale, and it would stop where the
|
|
35
|
+
issue's own text stops.
|
|
36
|
+
|
|
37
|
+
Reading that needs credentials, so the conversation is handed one secret,
|
|
38
|
+
`GITLAB_TOKEN`. `AGENT_SECRET_NAMES` is an allow-list, so the rest of the secret
|
|
39
|
+
store stays out of reach of a conversation whose instructions came from an issue.
|
|
40
|
+
Set it to `[]` for public projects if you would rather it held nothing.
|
|
41
|
+
|
|
42
|
+
The deployment's MCP servers are forwarded whole, as `github-pr-reviewer` does,
|
|
43
|
+
so a connected GitLab server gives the agent typed tools instead of curl. That
|
|
44
|
+
cuts both ways: everything reachable through those servers is reachable from a
|
|
45
|
+
prompt written by whoever opened the issue. Connect only servers you are willing
|
|
46
|
+
to have driven by untrusted text.
|
|
47
|
+
|
|
48
|
+
The agent commits, pushes its branch, and opens the merge request itself, so it
|
|
49
|
+
appears as soon as the agent stops instead of waiting for the next poll. The
|
|
50
|
+
script verifies that on GitLab rather than trusting the agent's word, and opens
|
|
51
|
+
the merge request itself when the agent did not - a failed push or a dead
|
|
52
|
+
conversation never loses the work. `origin` carries no credential, so each GitLab
|
|
53
|
+
command has to name `GITLAB_TOKEN`, which the SDK injects only into a command
|
|
54
|
+
that mentions it and masks in the output.
|
|
55
|
+
|
|
56
|
+
## Two setup paths
|
|
57
|
+
|
|
58
|
+
The `/issue-to-mr:setup` conversation substitutes the constants at the top of
|
|
59
|
+
`scripts/main.py` and uploads the result. The catalog entry
|
|
60
|
+
(`automations/catalog/gitlab-issue-to-mr/`) ships the same script unmodified as
|
|
61
|
+
a **bundle** and renders a `config.json` beside it from the setup form, which the
|
|
62
|
+
script loads over those constants. Both paths produce the same automation: a
|
|
63
|
+
tarball the automation service runs on a cron, not a prompt handed to an agent.
|
|
64
|
+
|
|
65
|
+
## Prerequisites
|
|
66
|
+
|
|
67
|
+
Set `GITLAB_TOKEN` in OpenHands Settings -> Secrets. It needs the `api` scope and
|
|
68
|
+
at least the **Developer** role on every watched project - Developer is the
|
|
69
|
+
lowest role that can push a branch and open a merge request. Leave the configured
|
|
70
|
+
branch prefix out of any protected-branch rule, or the push is rejected.
|
|
71
|
+
|
|
72
|
+
For a self-managed instance, give its API root
|
|
73
|
+
(`https://gitlab.example.com/api/v4`) during setup.
|
|
74
|
+
|
|
75
|
+
The automation runtime must have `git` available; the script clones, commits, and
|
|
76
|
+
pushes with it.
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
Ask OpenHands:
|
|
81
|
+
|
|
82
|
+
> "Set up an issue-to-MR automation for my `myorg/backend` GitLab project using
|
|
83
|
+
> the `openhands` label."
|
|
84
|
+
|
|
85
|
+
After setup, apply the configured label to an issue to queue an implementation.
|
|
86
|
+
To ask for another attempt later, remove and re-apply the label.
|
|
87
|
+
|
|
88
|
+
## See Also
|
|
89
|
+
|
|
90
|
+
- [SKILL.md](SKILL.md) - Full setup workflow reference
|
|
91
|
+
- [references/state-schema.md](references/state-schema.md) - State document and
|
|
92
|
+
task lifecycle
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gitlab-issue-to-mr
|
|
3
|
+
description: >
|
|
4
|
+
Create an automation that implements GitLab issues when a configurable
|
|
5
|
+
trigger label is applied. Polls one or more projects deterministically,
|
|
6
|
+
clones the default branch, starts one OpenHands conversation per label event,
|
|
7
|
+
then commits, pushes, and opens the merge request itself.
|
|
8
|
+
triggers:
|
|
9
|
+
- /issue-to-mr:setup
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# GitLab Issue to MR Automation
|
|
13
|
+
|
|
14
|
+
Create a cron automation that watches one or more GitLab projects for issues
|
|
15
|
+
with a trigger label, starts an OpenHands conversation once per label event with
|
|
16
|
+
the project's default branch already checked out, and opens a merge request with
|
|
17
|
+
whatever the agent produced.
|
|
18
|
+
|
|
19
|
+
The automation script is deterministic: issue discovery, label-event tracking,
|
|
20
|
+
state persistence, the clone, the branch, the commit, the push, the merge
|
|
21
|
+
request, the issue comments, and the clone's removal are all handled in Python.
|
|
22
|
+
The LLM is invoked only to write the code.
|
|
23
|
+
|
|
24
|
+
The agent is told **which** issue to implement, not what it says. It fetches the
|
|
25
|
+
description, the discussion, and whatever they link to itself, so nothing in the
|
|
26
|
+
prompt goes stale between dispatch and the moment the agent reads it.
|
|
27
|
+
|
|
28
|
+
That needs read access, so the conversation is handed one secret, `GITLAB_TOKEN`.
|
|
29
|
+
`AGENT_SECRET_NAMES` stays an allow-list: the rest of the deployment's secret
|
|
30
|
+
store is not reachable from a conversation whose instructions came from an issue.
|
|
31
|
+
|
|
32
|
+
The deployment's MCP servers are forwarded whole, matching `github-pr-reviewer`,
|
|
33
|
+
so a connected GitLab server gives the agent typed tools rather than curl. What
|
|
34
|
+
those servers reach is reachable from an issue-authored prompt, so connect only
|
|
35
|
+
servers that may be driven by untrusted text.
|
|
36
|
+
|
|
37
|
+
The agent also finishes the job: it commits, pushes its branch, and opens the
|
|
38
|
+
merge request, so the merge request appears when the agent stops rather than on
|
|
39
|
+
the next poll. The script does not trust that it happened - when the conversation
|
|
40
|
+
ends it asks GitLab whether the merge request exists, and opens it itself when it
|
|
41
|
+
does not. `origin` still carries no credential, so every GitLab command the agent
|
|
42
|
+
runs has to name `GITLAB_TOKEN`; the SDK only puts a secret in the environment of
|
|
43
|
+
a command that mentions it, and masks it in the output.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Prerequisites
|
|
48
|
+
|
|
49
|
+
### Required secret
|
|
50
|
+
|
|
51
|
+
Verify that the following secret is set in **OpenHands Settings -> Secrets**:
|
|
52
|
+
|
|
53
|
+
| Secret name | Token type | Minimum requirements |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `GITLAB_TOKEN` | Personal access token | `api` scope, and at least the **Developer** role on every watched project |
|
|
56
|
+
| `GITLAB_TOKEN` | Project or group access token | `api` scope, role **Developer** or above |
|
|
57
|
+
|
|
58
|
+
The `api` scope is what GitLab grants read and write on issues, notes, branches,
|
|
59
|
+
and merge requests through one scope; `read_api` polls happily and then fails at
|
|
60
|
+
the point of pushing. Developer is the lowest role that can push a branch and
|
|
61
|
+
open a merge request.
|
|
62
|
+
|
|
63
|
+
Two things the role does not cover, and which fail the push rather than the poll:
|
|
64
|
+
|
|
65
|
+
- **Protected branches.** The default branch is usually protected, but the
|
|
66
|
+
automation never pushes to it. Protect the branch prefix as well and the push
|
|
67
|
+
is rejected; leave `openhands/issue-*` unprotected.
|
|
68
|
+
- **CI/CD files.** An issue asking for a pipeline change makes the agent touch
|
|
69
|
+
`.gitlab-ci.yml`. That needs no extra scope, but a project with a protected
|
|
70
|
+
CI/CD configuration path rejects the push.
|
|
71
|
+
|
|
72
|
+
When several projects are monitored, the token must cover all of them.
|
|
73
|
+
|
|
74
|
+
Check with:
|
|
75
|
+
```bash
|
|
76
|
+
curl -s "https://gitlab.com/api/v4/user" \
|
|
77
|
+
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
78
|
+
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('username') or d.get('message'))"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If the token is missing or invalid, inform the user and stop.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Setup Workflow
|
|
86
|
+
|
|
87
|
+
Follow these steps in order.
|
|
88
|
+
|
|
89
|
+
### Step 1 - Verify `GITLAB_TOKEN`
|
|
90
|
+
|
|
91
|
+
Run the `curl` check above, against the user's instance if it is not
|
|
92
|
+
`gitlab.com`.
|
|
93
|
+
|
|
94
|
+
- If absent: *"GITLAB_TOKEN is not set. Please add it in OpenHands Settings ->
|
|
95
|
+
Secrets."* Stop.
|
|
96
|
+
- If the API returns `{"message": "401 Unauthorized"}`: tell the user the token
|
|
97
|
+
is invalid and ask them to update it. Stop.
|
|
98
|
+
|
|
99
|
+
### Step 2 - Collect the GitLab API URL
|
|
100
|
+
|
|
101
|
+
Ask: *"Which GitLab instance? (Press Enter for gitlab.com. For a self-managed
|
|
102
|
+
instance give its API root, e.g. `https://gitlab.example.com/api/v4`.)"*
|
|
103
|
+
|
|
104
|
+
Record as `GITLAB_API_URL`. Default: `https://gitlab.com/api/v4`. Use this URL in
|
|
105
|
+
every check below.
|
|
106
|
+
|
|
107
|
+
### Step 3 - Collect projects
|
|
108
|
+
|
|
109
|
+
Ask: *"Which GitLab projects should be watched?
|
|
110
|
+
(Format: `group/project`, e.g. `myorg/backend`. Subgroups are fine -
|
|
111
|
+
`myorg/team/service`. List several separated by commas to serve them all from one
|
|
112
|
+
automation.)"*
|
|
113
|
+
|
|
114
|
+
Validate access to **each** project, and confirm the token's role:
|
|
115
|
+
```bash
|
|
116
|
+
PROJECT_ID=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "{group}/{project}")
|
|
117
|
+
curl -s "${GITLAB_API_URL}/projects/${PROJECT_ID}" \
|
|
118
|
+
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
|
|
119
|
+
| python3 -c "
|
|
120
|
+
import json, sys
|
|
121
|
+
d = json.load(sys.stdin)
|
|
122
|
+
if 'message' in d or 'error' in d:
|
|
123
|
+
print('ERROR:', d.get('message') or d.get('error'))
|
|
124
|
+
else:
|
|
125
|
+
perms = d.get('permissions') or {}
|
|
126
|
+
levels = [(perms.get(k) or {}).get('access_level') for k in ('project_access', 'group_access')]
|
|
127
|
+
levels = [n for n in levels if isinstance(n, int)]
|
|
128
|
+
role = max(levels) if levels else 'unknown'
|
|
129
|
+
print(f\"Accessible. Default branch: {d.get('default_branch')}. Access level: {role}\")
|
|
130
|
+
"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Record every accepted project into `PROJECTS = ["{group}/{project}", ...]`. If
|
|
134
|
+
one project fails the check, say which and ask whether to continue without it.
|
|
135
|
+
An access level below `30` (Developer) means the automation cannot open merge
|
|
136
|
+
requests there; ask for a token with a higher role.
|
|
137
|
+
|
|
138
|
+
Each project is polled independently and keeps its own state, so issue numbers
|
|
139
|
+
never collide between them. The trigger label, branch prefix, and schedule are
|
|
140
|
+
shared; a project needing different settings wants its own automation.
|
|
141
|
+
|
|
142
|
+
### Step 4 - Collect trigger label
|
|
143
|
+
|
|
144
|
+
Ask: *"Which issue label should trigger an implementation?
|
|
145
|
+
(Press Enter for the default: `openhands`.)"*
|
|
146
|
+
|
|
147
|
+
Record the answer as `TRIGGER_LABEL`. If the label does not exist yet, tell the
|
|
148
|
+
user that GitLab will still record the event once the label is created and
|
|
149
|
+
applied to an issue.
|
|
150
|
+
|
|
151
|
+
The automation works an issue when it sees the latest matching label event for
|
|
152
|
+
that label. To ask for another attempt later, remove and re-apply the label -
|
|
153
|
+
that opens a second branch and a second merge request rather than overwriting the
|
|
154
|
+
first.
|
|
155
|
+
|
|
156
|
+
### Step 5 - Collect the merge request mode
|
|
157
|
+
|
|
158
|
+
Ask: *"Should the merge requests be opened as drafts?
|
|
159
|
+
1. Draft (default) - title prefixed `Draft:`, ready for a human to mark ready
|
|
160
|
+
2. Ready for review - opened as a normal merge request
|
|
161
|
+
(Press Enter for Draft)"*
|
|
162
|
+
|
|
163
|
+
Map the choice to `DRAFT_MERGE_REQUEST` (`True` or `False`). GitLab has no draft
|
|
164
|
+
flag on the merge request API; a draft is a title carrying the `Draft: ` prefix,
|
|
165
|
+
which the script adds.
|
|
166
|
+
|
|
167
|
+
### Step 6 - Collect the branch prefix
|
|
168
|
+
|
|
169
|
+
Ask: *"What branch prefix should the automation use?
|
|
170
|
+
(Press Enter for the default: `openhands/issue`, which produces
|
|
171
|
+
`openhands/issue-42`.)"*
|
|
172
|
+
|
|
173
|
+
Record as `BRANCH_PREFIX`. Keep it free of spaces and of characters git rejects
|
|
174
|
+
in a ref name, and make sure the prefix is not covered by a protected-branch
|
|
175
|
+
rule.
|
|
176
|
+
|
|
177
|
+
### Step 7 - Collect cron schedule
|
|
178
|
+
|
|
179
|
+
Ask: *"How often should the automation poll for labelled issues?
|
|
180
|
+
(Press Enter for the default: every 5 minutes.
|
|
181
|
+
Use a cron expression for a different interval, e.g. `0 * * * *` = hourly)"*
|
|
182
|
+
|
|
183
|
+
Default: `*/5 * * * *`.
|
|
184
|
+
|
|
185
|
+
Record as `CRON_SCHEDULE`.
|
|
186
|
+
|
|
187
|
+
### Step 8 - Confirm the secret scope
|
|
188
|
+
|
|
189
|
+
The agent is handed `GITLAB_TOKEN`, because it reads the issue and its discussion
|
|
190
|
+
itself. Ask: *"Beyond the GitLab token, does the project's build need a secret of
|
|
191
|
+
its own - a package registry token, for example? (Press Enter for none.)"*
|
|
192
|
+
|
|
193
|
+
Record the answers appended to the default, as
|
|
194
|
+
`AGENT_SECRET_NAMES = ["GITLAB_TOKEN", "NAME", ...]`.
|
|
195
|
+
|
|
196
|
+
Keep it an allow-list. Forwarding the whole secret store would put every
|
|
197
|
+
credential in the deployment behind a prompt written by whoever opened the issue.
|
|
198
|
+
If the projects are public and you would rather the conversation held no
|
|
199
|
+
credential at all, set the list to `[]` - the agent can still read a public issue
|
|
200
|
+
unauthenticated, and private projects then stop working.
|
|
201
|
+
|
|
202
|
+
The deployment's MCP servers are a separate matter: they are forwarded whole, so
|
|
203
|
+
the conversation can reach everything they expose. Say so, and check the user is
|
|
204
|
+
willing to have those servers driven by text written by whoever opened an issue.
|
|
205
|
+
Removing a server from the deployment's MCP settings is the only way to keep it
|
|
206
|
+
out of these conversations.
|
|
207
|
+
|
|
208
|
+
### Step 9 - Generate the automation script
|
|
209
|
+
|
|
210
|
+
Read `scripts/main.py` from this skill's directory. Apply exactly six constant
|
|
211
|
+
substitutions near the top of the file:
|
|
212
|
+
|
|
213
|
+
> The script also reads a `config.json` shipped beside it, if there is one, over
|
|
214
|
+
> these constants. That is how the catalog entry
|
|
215
|
+
> (`automations/catalog/gitlab-issue-to-mr/`) configures an unmodified copy,
|
|
216
|
+
> since a declarative host cannot rewrite Python. This setup path substitutes the
|
|
217
|
+
> constants and ships no `config.json`, so the two never collide.
|
|
218
|
+
|
|
219
|
+
| Placeholder | Replace with |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `PROJECTS = ["group/project"]` | `PROJECTS = ["{group_project}", ...]` - one entry per project collected in Step 3 |
|
|
222
|
+
| `TRIGGER_LABEL = "openhands"` | `TRIGGER_LABEL = "{trigger_label}"` |
|
|
223
|
+
| `BRANCH_PREFIX = "openhands/issue"` | `BRANCH_PREFIX = "{branch_prefix}"` |
|
|
224
|
+
| `DRAFT_MERGE_REQUEST = True` | `DRAFT_MERGE_REQUEST = {True or False}` |
|
|
225
|
+
| `GITLAB_API_URL = "https://gitlab.com/api/v4"` | `GITLAB_API_URL = "{gitlab_api_url}"` |
|
|
226
|
+
| `AGENT_SECRET_NAMES: list[str] = ["GITLAB_TOKEN"]` | `AGENT_SECRET_NAMES: list[str] = ["{name}", ...]` |
|
|
227
|
+
|
|
228
|
+
Leave `MAX_NEW_PER_RUN` and `DEFAULT_OPENHANDS_URL` alone unless the user asks
|
|
229
|
+
for a different cap or a non-default OpenHands URL.
|
|
230
|
+
|
|
231
|
+
A project may be given as `group/project`, as a clone URL, or as an SSH remote;
|
|
232
|
+
the script normalizes each one at startup and names the value it could not read
|
|
233
|
+
rather than blaming the token. Subgroups are preserved.
|
|
234
|
+
|
|
235
|
+
Use a safe string writer such as `json.dumps(value)` when inserting user-provided
|
|
236
|
+
project paths, labels, or prefixes into Python string literals.
|
|
237
|
+
`json.dumps(list_of_projects)` produces the whole `PROJECTS` list safely in one
|
|
238
|
+
step.
|
|
239
|
+
|
|
240
|
+
Write the customized script to a temporary build directory:
|
|
241
|
+
```bash
|
|
242
|
+
mkdir -p /tmp/issue-to-mr-build
|
|
243
|
+
# write the customized main.py to /tmp/issue-to-mr-build/main.py
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Validate syntax before packaging:
|
|
247
|
+
```bash
|
|
248
|
+
python3 -m py_compile /tmp/issue-to-mr-build/main.py && echo "Syntax OK"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Fix any syntax errors before proceeding.
|
|
252
|
+
|
|
253
|
+
### Step 10 - Package and upload
|
|
254
|
+
|
|
255
|
+
Determine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`
|
|
256
|
+
block in your system context:
|
|
257
|
+
- **OPENHANDS_HOST**: the Automation backend `url_from_agent`
|
|
258
|
+
- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
tar -czf /tmp/issue-to-mr.tar.gz -C /tmp/issue-to-mr-build .
|
|
262
|
+
|
|
263
|
+
TARBALL_PATH=$(curl -s -X POST \
|
|
264
|
+
"${OPENHANDS_HOST}/api/automation/v1/uploads?name=gitlab-issue-to-mr" \
|
|
265
|
+
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
|
|
266
|
+
-H "Content-Type: application/gzip" \
|
|
267
|
+
--data-binary @/tmp/issue-to-mr.tar.gz \
|
|
268
|
+
| python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
|
|
269
|
+
|
|
270
|
+
echo "Uploaded: $TARBALL_PATH"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Step 11 - Register the automation
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
|
|
277
|
+
-H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
|
|
278
|
+
-H "Content-Type: application/json" \
|
|
279
|
+
-d "{
|
|
280
|
+
\"name\": \"GitLab Issue to MR: {project_summary} label {trigger_label}\",
|
|
281
|
+
\"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\"},
|
|
282
|
+
\"tarball_path\": \"$TARBALL_PATH\",
|
|
283
|
+
\"entrypoint\": \"python3 main.py\",
|
|
284
|
+
\"timeout\": 900
|
|
285
|
+
}" | python3 -m json.tool
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Use the single project as `{project_summary}` when there is one, and something
|
|
289
|
+
like `3 projects` when there are several. A poll clones a project per queued
|
|
290
|
+
issue and pushes finished branches, so the timeout allows for that; a run never
|
|
291
|
+
waits for an agent to finish, only for it to be started.
|
|
292
|
+
|
|
293
|
+
Record the returned `id`.
|
|
294
|
+
|
|
295
|
+
### Step 12 - Confirm
|
|
296
|
+
|
|
297
|
+
Tell the user:
|
|
298
|
+
|
|
299
|
+
> ✅ **GitLab Issue to MR** is running!
|
|
300
|
+
>
|
|
301
|
+
> - Automation ID: `{id}`
|
|
302
|
+
> - Projects: `{group}/{project}`, ... (one line each)
|
|
303
|
+
> - GitLab API: `{gitlab_api_url}`
|
|
304
|
+
> - Trigger label: `{trigger_label}`
|
|
305
|
+
> - Branch prefix: `{branch_prefix}`
|
|
306
|
+
> - Merge requests: `{draft or ready for review}`
|
|
307
|
+
> - Polling schedule: `{cron_schedule}`
|
|
308
|
+
> - State file per project:
|
|
309
|
+
> `~/.openhands/workspaces/automation-state/gitlab_issue_to_mr_{id}_{group}__{project}.json`
|
|
310
|
+
>
|
|
311
|
+
> Apply the `{trigger_label}` label to an issue to queue an implementation. Each
|
|
312
|
+
> label event is processed once. To ask for another attempt, remove and re-apply
|
|
313
|
+
> the label - that opens a second branch and merge request.
|
|
314
|
+
>
|
|
315
|
+
> The agent runs without a checkout credential; the automation pushes the branch
|
|
316
|
+
> and opens the merge request once the agent has stopped.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Runtime Behaviour (per poll)
|
|
321
|
+
|
|
322
|
+
Each cron run executes `main.py`, which loads `config.json` if the catalog
|
|
323
|
+
shipped one, checks that `git` is available, resolves and validates `GITLAB_TOKEN`
|
|
324
|
+
once, then processes every project in `PROJECTS` independently. One project
|
|
325
|
+
failing does not stop the others; the run fails only if every project fails.
|
|
326
|
+
|
|
327
|
+
For each project:
|
|
328
|
+
|
|
329
|
+
1. Loads that project's state (see `references/state-schema.md`) and reads its
|
|
330
|
+
default branch and clone URL.
|
|
331
|
+
2. Lists open issues carrying `TRIGGER_LABEL`, newest-updated first. GitLab keeps
|
|
332
|
+
merge requests on their own endpoint, so labelling a merge request never
|
|
333
|
+
queues an implementation.
|
|
334
|
+
3. For each labelled issue, up to `MAX_NEW_PER_RUN` new ones per run:
|
|
335
|
+
- Refetches the issue so a label removed since the listing does not start work.
|
|
336
|
+
- Finds the latest matching resource label event with `action: "add"`, and
|
|
337
|
+
skips it if that event has already been tracked.
|
|
338
|
+
- Picks the first free branch name, `{BRANCH_PREFIX}-{iid}` or a numbered
|
|
339
|
+
variant of it.
|
|
340
|
+
- Clones the default branch, shallow and single-branch, into
|
|
341
|
+
`{WORKSPACE_BASE}/issue-to-mr/{group}__{project}/issue-{iid}-{event_id}`,
|
|
342
|
+
sets the commit identity, and creates the branch. `origin` keeps its plain
|
|
343
|
+
HTTPS URL, so the workspace holds no credential.
|
|
344
|
+
- Starts an OpenHands conversation **whose working directory is that clone**,
|
|
345
|
+
told which issue to read, with the secrets named in `AGENT_SECRET_NAMES`
|
|
346
|
+
and the deployment's MCP servers attached.
|
|
347
|
+
- Comments on the issue with the branch, the label event, and the conversation
|
|
348
|
+
link.
|
|
349
|
+
- Records the task with `status: "active"`.
|
|
350
|
+
- If the clone or the conversation cannot be created, the clone is removed and
|
|
351
|
+
nothing is recorded, so the next poll retries the label event.
|
|
352
|
+
4. For each active task:
|
|
353
|
+
- Abandons a conversation that has not reached a terminal status within two
|
|
354
|
+
hours, comments on the issue, and reclaims its clone.
|
|
355
|
+
- When the conversation reaches `idle`, `finished`, `error`, or `stuck`:
|
|
356
|
+
- Adopts the merge request the agent opened, if GitLab says one exists for
|
|
357
|
+
the branch, and comments its link on the issue. Everything below is the
|
|
358
|
+
path taken when it does not.
|
|
359
|
+
- Skips the merge request if the issue was closed meanwhile.
|
|
360
|
+
- Reports the problem on the issue if the conversation ended in `error` or
|
|
361
|
+
`stuck`.
|
|
362
|
+
- Commits whatever the agent left uncommitted, on top of any commits it made
|
|
363
|
+
itself.
|
|
364
|
+
- Posts the agent's answer on the issue, and opens no merge request, when
|
|
365
|
+
there are no commits at all - that is how an agent reports an issue too
|
|
366
|
+
ambiguous to implement.
|
|
367
|
+
- Otherwise pushes the branch, opens the merge request (draft by default,
|
|
368
|
+
titled `Draft: [#42] <issue title>`, with the agent's summary and
|
|
369
|
+
`Closes #42` in the description), and comments the link on the issue.
|
|
370
|
+
- A push or merge request that fails is retried on the next two polls before
|
|
371
|
+
the task is reported as failed, so a transient GitLab error does not throw
|
|
372
|
+
the work away.
|
|
373
|
+
5. Removes the clone of every finished task, but only after confirming the
|
|
374
|
+
conversation has stopped - deleting it under a running agent would remove its
|
|
375
|
+
working directory. When that cannot be confirmed the directory is left alone
|
|
376
|
+
and the next poll tries again.
|
|
377
|
+
6. Saves that project's state atomically.
|
|
378
|
+
|
|
379
|
+
The completion callback fires once for the whole run.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Additional Resources
|
|
384
|
+
|
|
385
|
+
- **`references/state-schema.md`** - State JSON schema, field definitions, and the
|
|
386
|
+
task lifecycle.
|
|
387
|
+
- **`scripts/main.py`** - The complete automation script. Customize the six
|
|
388
|
+
constants at the top before packaging.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Troubleshooting
|
|
393
|
+
|
|
394
|
+
| Symptom | Likely cause | Fix |
|
|
395
|
+
|---|---|---|
|
|
396
|
+
| Nothing is ever queued | Trigger label not present, or applied to a merge request rather than an issue | Apply the configured label to an issue |
|
|
397
|
+
| "401 Unauthorized" in run logs | Token expired | Rotate and update `GITLAB_TOKEN` |
|
|
398
|
+
| "The token's role on ... is below Developer" | The token has Reporter or Guest on that project | Grant Developer or above, or drop the project from `PROJECTS` |
|
|
399
|
+
| Push rejected: "You are not allowed to push code to protected branches" | The branch prefix is covered by a protected-branch rule | Leave `{BRANCH_PREFIX}-*` unprotected |
|
|
400
|
+
| Push rejected on `.gitlab-ci.yml` | The project protects its CI/CD configuration path | Allow the token's role to update it, or exclude such issues |
|
|
401
|
+
| 404 on project access | Project path wrong, or no access | Re-check the entry in `PROJECTS` and the token's role. Subgroups must be included in full |
|
|
402
|
+
| `git is not available in the automation runtime` | The runtime image has no git | Use a runtime image that ships git; the script clones, commits, and pushes with it |
|
|
403
|
+
| Issue commented "did not change any code" | The agent judged the issue too ambiguous, or made no edits | Read its answer in the comment, add the missing detail to the issue, then re-apply the label |
|
|
404
|
+
| Same issue not picked up again after new comments | Its label event was already processed | Remove and re-apply the trigger label |
|
|
405
|
+
| Agent reports it cannot push or open an MR | By design - it has no push credentials in `origin` | No action; the automation pushes and opens the merge request after the agent stops |
|
|
406
|
+
| `Warning: could not fetch MCP config` in run logs | The settings endpoint was unreachable | Non-fatal; the agent falls back to the REST calls in the prompt |
|
|
407
|
+
| A backlog of labelled issues starts slowly | `MAX_NEW_PER_RUN` caps how many conversations one poll starts | Wait for the next polls, or raise the cap in the script |
|
|
408
|
+
| Clones remain under `issue-to-mr/` | Their conversations had not stopped yet | They are removed by a later poll once the conversation is terminal |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
# auto-generated by sync_extensions.py
|
|
3
|
+
description: Create an automation that implements GitLab issues when a configurable trigger label is applied. Polls one or more projects deterministically, clones the default branch, starts one OpenHands conversation per label event, then commits, pushes, and opens the merge request itself.
|
|
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,140 @@
|
|
|
1
|
+
# State Schema
|
|
2
|
+
|
|
3
|
+
The automation maintains a JSON state document **per project**, persisted across
|
|
4
|
+
polling runs. It is the source of truth for which trigger-label events have
|
|
5
|
+
already queued work, which conversations are still active, and which clones are
|
|
6
|
+
still on disk.
|
|
7
|
+
|
|
8
|
+
Each project in `PROJECTS` gets its own document, so issue IIDs from different
|
|
9
|
+
projects never share a bucket.
|
|
10
|
+
|
|
11
|
+
The document holds only what a poll needs in order to decide what to do next.
|
|
12
|
+
Issue metadata that can be read back from GitLab is not mirrored here, so there
|
|
13
|
+
is nothing to drift.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Storage
|
|
18
|
+
|
|
19
|
+
**Primary (cloud):** The state is stored in the automation service's built-in KV
|
|
20
|
+
store under the key `state:{group}__{project}` - for example
|
|
21
|
+
`state:gitlab-org__gitlab`. Subgroups keep their separators, so
|
|
22
|
+
`group/team/service` becomes `state:group__team__service`. The KV store is
|
|
23
|
+
available when `AUTOMATION_KV_TOKEN` is injected into the run environment. Each
|
|
24
|
+
automation has its own isolated namespace.
|
|
25
|
+
|
|
26
|
+
**Fallback (local/dev):** When the KV store is not available, the state is
|
|
27
|
+
written to a local JSON file at:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
{WORKSPACE_BASE_ROOT}/automation-state/gitlab_issue_to_mr_{automation_id}_{group}__{project}.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`WORKSPACE_BASE_ROOT` is derived by going two levels up from the `WORKSPACE_BASE`
|
|
34
|
+
environment variable, stripping `automation-runs/{run_id}`.
|
|
35
|
+
|
|
36
|
+
Example on a local install:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
~/.openhands/workspaces/automation-state/gitlab_issue_to_mr_abc12345-..._myorg__backend.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `automation_id` is read from the `AUTOMATION_EVENT_PAYLOAD` environment
|
|
43
|
+
variable, field `automation_id`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Top-Level Schema
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
{
|
|
51
|
+
"version": 1,
|
|
52
|
+
"project": "group/project",
|
|
53
|
+
"trigger_label": "openhands",
|
|
54
|
+
"updated_at": 1717200000.0,
|
|
55
|
+
"tasks": {}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## `tasks` Map
|
|
62
|
+
|
|
63
|
+
Key: `"{issue_iid}:label:{label_event_id}"`. This makes the latest GitLab
|
|
64
|
+
resource label event with `action: "add"` the idempotency key. Re-applying the
|
|
65
|
+
trigger label creates a new event and therefore a new task, on a new branch.
|
|
66
|
+
|
|
67
|
+
Value: **TaskRecord**
|
|
68
|
+
|
|
69
|
+
```jsonc
|
|
70
|
+
{
|
|
71
|
+
"issue_iid": 42,
|
|
72
|
+
"issue_title": "Retry uploads on a 502",
|
|
73
|
+
"trigger_label_event_id": 123456789,
|
|
74
|
+
"trigger_label_event_created_at": "2026-06-12T00:00:00Z",
|
|
75
|
+
"web_url": "https://gitlab.com/group/project/-/issues/42",
|
|
76
|
+
"base_branch": "main",
|
|
77
|
+
"base_sha": "0123456789abcdef...",
|
|
78
|
+
"branch": "openhands/issue-42",
|
|
79
|
+
"status": "active",
|
|
80
|
+
"conversation_id": "conv_abc123",
|
|
81
|
+
"workspace_dir": "/workspace/issue-to-mr/group__project/issue-42-123456789",
|
|
82
|
+
"last_activity": 1717200000.0,
|
|
83
|
+
"finalize_attempts": 1,
|
|
84
|
+
"merge_request_url": "https://gitlab.com/group/project/-/merge_requests/57",
|
|
85
|
+
"merge_request_iid": 57,
|
|
86
|
+
"completed_at": 1717203600.0,
|
|
87
|
+
"expired_after": 7200.5,
|
|
88
|
+
"error": "git push origin ... failed (128): ..."
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Field | Written when | Meaning |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `issue_iid` | claim | The issue being implemented, by its project-scoped IID |
|
|
95
|
+
| `issue_title` | claim | Used for the commit message and the merge request title |
|
|
96
|
+
| `trigger_label_event_id` | claim | The GitLab label event this task belongs to |
|
|
97
|
+
| `trigger_label_event_created_at` | claim | When that label was applied |
|
|
98
|
+
| `web_url` | claim | The issue URL |
|
|
99
|
+
| `base_branch` | claim | The project's default branch at claim time |
|
|
100
|
+
| `base_sha` | start | The commit the clone starts from; commits are counted against it |
|
|
101
|
+
| `branch` | start | The branch the merge request is opened from |
|
|
102
|
+
| `status` | throughout | See the lifecycle below |
|
|
103
|
+
| `conversation_id` | start | The OpenHands conversation doing the work |
|
|
104
|
+
| `workspace_dir` | start | The clone. Removed, and the field dropped, once the task ends |
|
|
105
|
+
| `last_activity` | throughout | Drives the debounce, the two-hour abandonment, and the stalled-claim release |
|
|
106
|
+
| `finalize_attempts` | finalize | How many times pushing and opening the merge request has been tried |
|
|
107
|
+
| `merge_request_url` / `merge_request_iid` | success | The merge request that was opened |
|
|
108
|
+
| `completed_at` | end | When the task reached a terminal status |
|
|
109
|
+
| `expired_after` | expiry | Seconds the conversation ran before being abandoned |
|
|
110
|
+
| `error` | failure | The last finalization error, after the retries ran out |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Task Lifecycle
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
label event seen
|
|
118
|
+
│
|
|
119
|
+
▼
|
|
120
|
+
"starting" ── claim persisted before the slow work, so an
|
|
121
|
+
│ overlapping poll cannot start it twice.
|
|
122
|
+
│ Released after 15 minutes if the poll died.
|
|
123
|
+
clone + conversation
|
|
124
|
+
│
|
|
125
|
+
▼
|
|
126
|
+
"active"
|
|
127
|
+
│
|
|
128
|
+
┌─────────────┬───────┴────────┬──────────────┬─────────────────┐
|
|
129
|
+
▼ ▼ ▼ ▼ ▼
|
|
130
|
+
"issue-closed" "failed" "no-changes" "closed" "expired"
|
|
131
|
+
issue closed conversation agent produced merge request no terminal
|
|
132
|
+
meanwhile errored, or no commits; opened and status within
|
|
133
|
+
push/MR gave its answer is linked on the two hours
|
|
134
|
+
up after 3 posted on the issue
|
|
135
|
+
attempts issue
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Every terminal status releases the clone, but only once the conversation is
|
|
139
|
+
confirmed stopped. When that cannot be confirmed, `workspace_dir` stays in the
|
|
140
|
+
record and a later poll retries the removal.
|