@openhands/extensions 0.29.0 → 0.31.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.
Files changed (97) hide show
  1. package/.agents/skills/custom-codereview-guide.md +47 -5
  2. package/.claude-plugin/marketplace.json +20 -5
  3. package/.codex-plugin/marketplace.json +20 -5
  4. package/.github/workflows/build-vscode-app.yml +3 -3
  5. package/.github/workflows/check-extensions.yml +1 -1
  6. package/.github/workflows/pr-artifacts.yml +4 -83
  7. package/.github/workflows/tests.yml +14 -1
  8. package/.plugin/marketplace.json +20 -5
  9. package/.release-please-manifest.json +1 -1
  10. package/AGENTS.md +4 -3
  11. package/README.md +6 -5
  12. package/apps/vscode/README.md +8 -6
  13. package/apps/vscode/canvas-extension.json +11 -3
  14. package/apps/vscode/extension.js +60 -12
  15. package/apps/vscode/scripts/package.mjs +20 -74
  16. package/automations/bundle-index.js +10 -8
  17. package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
  18. package/automations/catalog/github-issue-triage/manifest.json +4 -3
  19. package/automations/catalog/github-pr-reviewer/manifest.json +7 -6
  20. package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
  21. package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
  22. package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
  23. package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
  24. package/integrations/catalog/atlassian-rovo.json +4 -3
  25. package/integrations/catalog/bitbucket.json +16 -1
  26. package/integrations/catalog/github.json +1 -0
  27. package/integrations/catalog/gitlab.json +1 -1
  28. package/marketplaces/openhands-extensions.json +20 -5
  29. package/package.json +1 -1
  30. package/plugins/pr-review/README.md +1 -0
  31. package/plugins/pr-review/scripts/prompt.py +44 -11
  32. package/plugins/runtime-api-configs/SKILL.md +1 -1
  33. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
  34. package/pyproject.toml +1 -1
  35. package/python/openhands_extensions/_version.py +1 -1
  36. package/skills/canvas-extension-api/references/backend-safety.md +3 -3
  37. package/skills/canvas-extension-api/references/v1-contract.md +7 -4
  38. package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
  39. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
  40. package/skills/github/scripts/agent_conversation.py +224 -8
  41. package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
  42. package/skills/github-issue-triage/README.md +73 -14
  43. package/skills/github-issue-triage/SKILL.md +26 -46
  44. package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
  45. package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
  46. package/skills/github-issue-triage/scripts/worker.py +153 -103
  47. package/skills/github-pr-reviewer/README.md +109 -1
  48. package/skills/github-pr-reviewer/SKILL.md +42 -8
  49. package/skills/github-pr-reviewer/references/state-schema.md +8 -0
  50. package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
  51. package/skills/github-pr-reviewer/scripts/main.py +163 -39
  52. package/skills/github-pr-reviewer/scripts/publication.py +193 -0
  53. package/skills/github-pr-reviewer/scripts/worker.py +297 -8
  54. package/skills/github-pr-reviewer/tests/test_main.py +73 -5
  55. package/skills/github-stale-ci-pr-closer/scripts/worker.py +8 -2
  56. package/skills/gitlab-issue-to-mr/README.md +10 -3
  57. package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
  58. package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
  59. package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
  60. package/skills/index.js +18 -8
  61. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
  62. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
  63. package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
  64. package/skills/jira-issue-to-pr/README.md +18 -11
  65. package/skills/jira-issue-to-pr/SKILL.md +72 -23
  66. package/skills/jira-issue-to-pr/references/setup.md +43 -12
  67. package/skills/jira-issue-to-pr/scripts/main.py +136 -26
  68. package/skills/slack-channel-monitor/README.md +12 -3
  69. package/skills/slack-channel-monitor/SKILL.md +35 -20
  70. package/skills/slack-channel-monitor/references/windows.md +6 -2
  71. package/skills/slack-channel-monitor/scripts/main.py +189 -12
  72. package/skills/software-factory/.claude-plugin/plugin.json +19 -0
  73. package/skills/software-factory/.codex-plugin/plugin.json +19 -0
  74. package/skills/software-factory/.plugin/plugin.json +19 -0
  75. package/skills/software-factory/README.md +64 -0
  76. package/skills/software-factory/SKILL.md +229 -0
  77. package/skills/software-factory/commands/software-factory.md +8 -0
  78. package/skills/software-factory/scripts/factory.py +630 -0
  79. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
  80. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
  81. package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
  82. package/skills/ticket-to-code-change/SKILL.md +5 -3
  83. package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
  84. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  85. package/tests/test_agent_conversation_dispatch.py +173 -6
  86. package/tests/test_automation_llm_provenance.py +1209 -0
  87. package/tests/test_github_review_publication.py +422 -0
  88. package/tests/test_github_reviewer_delivery.py +174 -21
  89. package/tests/test_github_stale_ci_pr_closer.py +52 -1
  90. package/tests/test_github_triage_delivery.py +411 -238
  91. package/tests/test_gitlab_issue_to_mr.py +284 -0
  92. package/tests/test_jira_issue_to_pr.py +200 -6
  93. package/tests/test_pr_review_prompt_injection.py +353 -0
  94. package/tests/test_slack_channel_monitor.py +32 -1
  95. package/tests/test_software_factory_script.py +231 -0
  96. package/tests/test_vscode_app.py +8 -2
  97. package/tests/vscode-app.test.mjs +8 -2
@@ -1,10 +1,12 @@
1
- # Jira Issue to GitHub PR
1
+ # Jira Issue to Pull Request (GitHub, GitLab, Bitbucket)
2
2
 
3
3
  Deploy a cron-based OpenHands automation that polls a Jira Cloud project for
4
4
  open issues carrying a configurable label (default: `create-pr`) and, for each
5
- new issue, starts an OpenHands agent conversation that clones the GitHub
6
- repository named in the ticket body, creates a branch, implements the requested
7
- change, and opens a pull request. Once the conversation starts, it posts a
5
+ new issue, starts an OpenHands agent conversation that clones the repository
6
+ named in the ticket body on the configured git provider (GitHub by default,
7
+ GitLab or Bitbucket with `git_provider`), creates a branch, implements the
8
+ requested change, and opens a pull request (a merge request on GitLab). Once the
9
+ conversation starts, it posts a
8
10
  comment on the Jira ticket: "I'm on it: `<conversation URL>`".
9
11
 
10
12
  ## Triggers
@@ -16,12 +18,16 @@ This skill is activated by keywords:
16
18
  - `automatically create GitHub PRs from Jira tickets`
17
19
  - `deploy a Jira issue-to-PR automation`
18
20
  - `create a Jira to GitHub PR workflow`
21
+ - `set up the Jira issue to GitLab MR automation`
22
+ - `set up the Jira issue to Bitbucket PR automation`
19
23
  - `automate GitHub PR creation from a Jira label`
20
24
 
21
25
  ## How It Works
22
26
 
23
- 1. **Poll** - every N minutes, the script calls `POST /rest/api/3/search/jql`
24
- on the Jira Cloud instance to find open issues with the configured label.
27
+ 1. **Poll** - every N minutes, the script searches the Jira Cloud instance for
28
+ open issues with the configured label: through the user's connected
29
+ Atlassian Rovo MCP server on OpenHands Cloud and Enterprise, or with
30
+ `POST /rest/api/3/search/jql` and an API token.
25
31
  2. **Deduplicate** - on the first run the script records a `first_run_at`
26
32
  baseline timestamp in the KV store; issues whose `updated` timestamp
27
33
  predates that baseline are skipped (no backfill blast on first deploy).
@@ -32,7 +38,8 @@ This skill is activated by keywords:
32
38
  conversation with a PR-creation prompt: `POST /api/conversations` on the
33
39
  agent server when running locally, `POST /api/v1/app-conversations` on
34
40
  OpenHands Cloud and Enterprise. The prompt instructs the agent to extract the
35
- target GitHub repository (`owner/repo`) from the ticket body. On Cloud the
41
+ target repository (`owner/repo` on GitHub; `group/project` or a URL on
42
+ GitLab; `workspace/repo` on Bitbucket) from the ticket body. On Cloud the
36
43
  next poll confirms the conversation came up and starts it again when it did
37
44
  not.
38
45
  4. **Comment** - immediately after the conversation is created, the script
@@ -52,13 +59,13 @@ OpenHands and Jira. See "Event-Based Alternative" in `SKILL.md`.
52
59
 
53
60
  | Requirement | Details |
54
61
  |---|---|
55
- | **Jira API token** | Stored as an OpenHands secret (see `references/setup.md`) |
56
- | **GitHub token** | Stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs |
62
+ | **Jira access** | OpenHands Cloud and Enterprise: the user's connected Atlassian Rovo MCP server, with no API token. Local, or without that connection: a Jira API token stored as an OpenHands secret (see `references/setup.md`) |
63
+ | **Git provider access** | OpenHands Cloud and Enterprise: the connected GitHub, GitLab or Bitbucket integration named by `git_provider`, or a connected MCP server (GitHub's, GitLab's, or Atlassian Rovo for Bitbucket). Local: that provider's token stored as an OpenHands secret so the spawned conversation can push branches and open the request |
57
64
  | **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |
58
- | **GitHub repo** | The target repository must exist and the GitHub token must have write access |
65
+ | **Target repository** | Must exist on the configured provider; the connected integration or token must have write access |
59
66
 
60
67
  ## Quick Start
61
68
 
62
69
  See `SKILL.md` for the full step-by-step deployment guide, and
63
- `references/setup.md` for Jira API token creation, GitHub token scopes, cron
70
+ `references/setup.md` for Jira API token creation, git provider tokens, cron
64
71
  schedule reference, and troubleshooting.
@@ -4,25 +4,31 @@ description: >
4
4
  This skill should be used when the user asks to "set up a Jira automation to create pull requests",
5
5
  "poll Jira for create-pr issues", "automatically create GitHub PRs from Jira tickets",
6
6
  "deploy a Jira issue-to-PR automation", "create a Jira to GitHub PR workflow",
7
- or mentions automating GitHub PR creation from a Jira label.
7
+ "set up the Jira issue to GitLab MR automation", "set up the Jira issue to Bitbucket PR automation",
8
+ "open GitLab merge requests from Jira tickets", "open Bitbucket pull requests from Jira tickets",
9
+ or mentions automating pull request or merge request creation from a Jira label.
8
10
  Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues
9
11
  labeled with a configurable label (default: "create-pr") and spawns an agent conversation
10
- to create a GitHub pull request for each new issue found. The target GitHub repository
11
- is read from the body of the Jira ticket - no repo parameter is required at deploy time.
12
+ that opens a pull request on GitHub (the default), a merge request on GitLab, or a pull
13
+ request on Bitbucket for each new issue found, chosen with git_provider. The target
14
+ repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.
12
15
  ---
13
16
 
14
- # Jira → GitHub PR Automation
17
+ # Jira → Pull Request Automation (GitHub, GitLab, Bitbucket)
15
18
 
16
19
  Deploys a cron automation that polls a Jira Cloud instance for open issues carrying a
17
20
  configurable label and, for each new issue, starts an OpenHands agent conversation that
18
- clones the GitHub repository specified in the ticket body, creates a branch, implements
19
- or placeholders the requested change, and opens a pull request. Once the conversation
21
+ clones the repository specified in the ticket body on the configured git provider (GitHub
22
+ by default, GitLab or Bitbucket with `git_provider`), creates a branch, implements or
23
+ placeholders the requested change, and opens a pull request (a merge request on GitLab).
24
+ Once the conversation
20
25
  starts, it also posts a comment on the Jira ticket: "I'm on it: &lt;conversation URL&gt;".
21
26
 
22
27
  ## How It Works
23
28
 
24
- 1. **Poll** - every N minutes, `POST /rest/api/3/search/jql` on the Jira Cloud instance
25
- to find open issues with the configured label.
29
+ 1. **Poll** - every N minutes, search the Jira Cloud instance for open issues with the
30
+ configured label: through the user's connected Atlassian Rovo MCP server on OpenHands
31
+ Cloud and Enterprise, or with `POST /rest/api/3/search/jql` and an API token.
26
32
  2. **Deduplicate** - on the very first run the script records a `first_run_at` baseline
27
33
  timestamp in the KV store; any issue whose `updated` timestamp predates that baseline
28
34
  is skipped (no backfill blast on first deploy). Using `updated` rather than `created`
@@ -33,7 +39,8 @@ starts, it also posts a comment on the Jira ticket: "I'm on it: &lt;conversation
33
39
  3. **Dispatch** - for each new issue, start an independent agent conversation with a
34
40
  PR-creation prompt: `POST /api/conversations` on the agent server when running locally,
35
41
  or `POST /api/v1/app-conversations` on OpenHands Cloud. The prompt instructs the agent
36
- to extract the target GitHub repository (`owner/repo`) from the ticket body.
42
+ to extract the target repository (`owner/repo` on GitHub; `group/project` or a URL on GitLab;
43
+ `workspace/repo` on Bitbucket) from the ticket body.
37
44
  4. **Comment** - immediately after the conversation is created, post a Jira comment on the
38
45
  issue: `I'm on it: <conversation URL>`.
39
46
  5. **Persist** - record the processed issue key so re-runs never duplicate work.
@@ -47,11 +54,11 @@ Before deploying, ensure the following are in place:
47
54
 
48
55
  | Requirement | Details |
49
56
  |---|---|
50
- | **Jira API token** | Stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |
51
- | **GitHub access** | Local: a GitHub token stored as an OpenHands secret with `repo` + `workflow` scope so the spawned conversation can push branches and open PRs. OpenHands Cloud: the spawned conversation uses the user's connected GitHub integration (native integration recommended, or the GitHub MCP server) |
57
+ | **Jira access** | OpenHands Cloud and Enterprise: the user's connected Atlassian Rovo MCP server, with no API token. Local, or without that connection: a Jira API token stored as an OpenHands secret (see [Jira API token setup](#jira-api-token)) |
58
+ | **Git provider access** | OpenHands Cloud and Enterprise: the user's connected GitHub, GitLab or Bitbucket integration, whichever `git_provider` names. A connected MCP server also works - GitHub's (with the saved access token), GitLab's, or Atlassian Rovo for Bitbucket - with a private repository read through the tools rather than cloned. Local: a token for that provider stored as an OpenHands secret (`GITHUB_TOKEN` with `repo` + `workflow`, `GITLAB_TOKEN` with `api`, `BITBUCKET_TOKEN`) so the spawned conversation can push branches and open the request |
52
59
  | **KV store** (OpenHands Cloud only) | The automation service must have its KV store enabled (`kvStore` in `GET /api/automation/v1/capabilities`); each cloud run starts in a fresh sandbox, so processed issues are remembered there |
53
60
  | **Jira label** | The label to watch for (default: `create-pr`) must exist in the Jira project |
54
- | **GitHub repo** | The target repository must exist and the GitHub token must have write access |
61
+ | **Target repository** | Must exist on the configured provider; the connected integration or token must have write access |
55
62
 
56
63
  ## Deploying the Automation
57
64
 
@@ -72,24 +79,53 @@ Set `OPENHANDS_HOST` and `AUTH_HEADER` for the curl commands below:
72
79
 
73
80
  ### Step 1 - Collect parameters
74
81
 
75
- Gather the following from the user before proceeding:
82
+ First decide how the poller reaches Jira:
83
+
84
+ - **Connected Atlassian Rovo MCP server** - on OpenHands Cloud and Enterprise, when the
85
+ user has connected it. OpenHands calls Jira for the poller through that connection, so
86
+ do not ask for a Jira email or an API token and do not create a secret.
87
+ - **Jira API token** - on a local Agent Canvas, or when the Rovo server is not connected.
88
+
89
+ Then pick `git_provider` from the automation the user asked for: `github` for *Jira issue to
90
+ GitHub PR* (the default, so it can be left out), `gitlab` for *Jira issue to GitLab MR*,
91
+ `bitbucket` for *Jira issue to Bitbucket PR*. It changes only what the conversation is told;
92
+ the conversation pushes with the user's connected integration for that provider, or delivers
93
+ through the connected MCP server's tools when it has no token.
94
+
95
+ Then gather the parameters for that route, plus the shared ones, before proceeding:
76
96
 
77
97
  | Parameter | Example | Notes |
78
98
  |---|---|---|
79
- | `jira_base_url` | `https://acme.atlassian.net` | No trailing slash |
80
- | `jira_email` | `alice@acme.com` | Atlassian account email for Basic auth |
81
- | `jira_token_secret` | `JIRA_CLOUD_KEY` | Name of the OpenHands secret holding the API token |
99
+ | `jira_mcp_server` | `atlassian-rovo` | Rovo connection only. The name the server is stored under: the key in `agent_settings.mcp_config` of `GET ${OPENHANDS_HOST}/api/v1/settings` whose `url` is on `mcp.atlassian.com`. Look it up; do not ask the user |
100
+ | `jira_cloud_id` | `1324a887-45db-1bf4-1e99-ef0ff456d421` | Rovo connection only. The `id` of the Jira site, from the Rovo tool `getAccessibleAtlassianResources`. Ask which site only when it lists several |
101
+ | `jira_base_url` | `https://acme.atlassian.net` | API token only. No trailing slash |
102
+ | `jira_email` | `alice@acme.com` | API token only. Atlassian account email for Basic auth |
103
+ | `jira_token_secret` | `JIRA_CLOUD_KEY` | API token only. Name of the OpenHands secret holding the API token |
82
104
  | `jira_label` | `create-pr` | Label to watch for (optional, defaults to `create-pr`) |
105
+ | `git_provider` | `gitlab` | `github` (default), `gitlab` or `bitbucket` - the provider the ticket names its repository on (optional) |
83
106
  | `max_new_per_run` | `5` | Max conversations dispatched per cron firing (optional, defaults to `5`) |
84
107
  | `cron_schedule` | `*/5 * * * *` | Polling frequency in cron syntax |
85
108
 
86
- > **Note**: The GitHub repository is not configured here. Each Jira ticket body must include
87
- > a reference to the target GitHub repo in `owner/repo` format (e.g. `acme-org/backend`).
88
- > The spawned agent extracts it from the ticket text.
109
+ > **Note**: The repository is not configured here. Each Jira ticket body must name it:
110
+ > `owner/repo` on GitHub, `group/subgroup/project` or a full URL on GitLab (self-managed
111
+ > hosts included), `workspace/repo` on bitbucket.org. The spawned agent extracts it from
112
+ > the ticket text.
89
113
 
90
114
  ### Step 2 - Create config.json
91
115
 
92
- Create `config.json` next to `scripts/main.py` when packaging:
116
+ Create `config.json` next to `scripts/main.py` when packaging. With the connected
117
+ Atlassian Rovo MCP server:
118
+
119
+ ```json
120
+ {
121
+ "jira_mcp_server": "atlassian-rovo",
122
+ "jira_cloud_id": "1324a887-45db-1bf4-1e99-ef0ff456d421",
123
+ "jira_label": "create-pr",
124
+ "max_new_per_run": 5
125
+ }
126
+ ```
127
+
128
+ With a Jira API token:
93
129
 
94
130
  ```json
95
131
  {
@@ -101,6 +137,9 @@ Create `config.json` next to `scripts/main.py` when packaging:
101
137
  }
102
138
  ```
103
139
 
140
+ For the GitLab and Bitbucket templates add `"git_provider": "gitlab"` or
141
+ `"git_provider": "bitbucket"` to either example; it is `github` when omitted.
142
+
104
143
  ### Step 3 - Package the tarball
105
144
 
106
145
  Copy `scripts/main.py` from this skill and package it with the `config.json`:
@@ -143,7 +182,8 @@ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
143
182
  }" | python3 -m json.tool
144
183
  ```
145
184
 
146
- Save the returned `id` - use it for updates and monitoring.
185
+ Save the returned `id` - use it for updates and monitoring. Name the automation after the
186
+ template the user picked, for example `Jira issue-to-MR Poller` for GitLab.
147
187
 
148
188
  ### Step 6 - Verify with a test dispatch
149
189
 
@@ -158,6 +198,14 @@ curl -s "${OPENHANDS_HOST}/api/automation/v1/<AUTOMATION_ID>/runs?limit=1" \
158
198
  | python3 -c "import sys,json; r=json.load(sys.stdin)['runs'][0]; print(r['status'], r.get('error_detail'))"
159
199
  ```
160
200
 
201
+ With the Rovo connection, a run that fails on `/api/v1/mcp/servers/<name>/call-tool` says why:
202
+
203
+ - `404` with `No connected MCP server is named ...` - `jira_mcp_server` is not the name the
204
+ server is stored under, or the user switched the server off.
205
+ - `404` with `Not Found` - this deployment cannot call a connected MCP server for a run yet.
206
+ Set the automation up with a Jira API token instead.
207
+ - `502` - the server could not be reached, or the user has to reconnect it in the settings.
208
+
161
209
  ## Event-Based Alternative
162
210
 
163
211
  Where Jira can reach the deployment over the internet (OpenHands Cloud and Enterprise),
@@ -287,7 +335,8 @@ The automation script lives at `scripts/main.py`. Key behaviors:
287
335
  - **First-run baseline** - on the very first execution the script writes `first_run_at` (UTC timestamp) into the KV store and exits without dispatching; issues whose `updated` timestamp predates that baseline are skipped on all subsequent runs. Using `updated` (not `created`) means an old issue that has its label applied after deployment is correctly treated as new.
288
336
  - **Per-run cap** - `max_new_per_run` (default 5) limits how many conversations are started per cron firing; any remaining new issues are dispatched on the next run.
289
337
  - **KV store** - persists `{"processed_keys": [...], "first_run_at": "..."}` between runs; falls back to a local file in local dev environments where `AUTOMATION_KV_TOKEN` is absent (on OpenHands Cloud the run fails instead, since its sandbox does not persist).
290
- - **Jira API** - uses `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).
338
+ - **Jira access** - with `jira_mcp_server` set, searches and comments through the user's connected Atlassian Rovo MCP server (`POST /api/v1/mcp/servers/<name>/call-tool` on OpenHands Cloud, tools `searchJiraIssuesUsingJql` and `addOrEditJiraIssueComment`, or `addCommentToJiraIssue` on a server that still offers the v1 tools), so the run reads no Jira credential. Otherwise uses the Jira REST API with the token secret: `POST /rest/api/3/search/jql` (the current non-deprecated endpoint).
339
+ - **Git provider** - `git_provider` selects the prompt's wording (GitHub pull request, GitLab merge request, Bitbucket pull request), the repository formats the agent looks for, and how it delivers without a token (GitLab's MCP tools, Rovo's Bitbucket tools); nothing else differs between the three templates.
291
340
  - **Conversation dispatch** - locally, calls `POST /api/conversations` on the agent server with the current user's LLM/agent settings forwarded to the new conversation; on OpenHands Cloud, calls `POST /api/v1/app-conversations`, which runs each conversation in its own sandbox with the user's settings, secrets and connected git provider.
292
341
  - **Start confirmation** (OpenHands Cloud only) - the API answers a start request before the conversation exists, so the next run checks each one. A start that failed is made again, up to three times, and after that the failure is posted on the Jira issue.
293
342
  - **Error transparency** - captures Jira HTTP response bodies in error messages for fast diagnosis.
@@ -324,4 +373,4 @@ that issue again.
324
373
 
325
374
  ## Additional Resources
326
375
 
327
- - **`references/setup.md`** - Jira API token creation, GitHub token scopes, cron schedule reference, and troubleshooting guide.
376
+ - **`references/setup.md`** - Jira API token creation, git provider tokens, cron schedule reference, and troubleshooting guide.
@@ -2,6 +2,9 @@
2
2
 
3
3
  ## Jira API Token
4
4
 
5
+ The poller needs this token only when it does not reach Jira through a connected
6
+ Atlassian Rovo MCP server (see Step 1 in `SKILL.md`).
7
+
5
8
  ### Create a token
6
9
 
7
10
  1. Log in to [id.atlassian.com](https://id.atlassian.com/manage-profile/security/api-tokens).
@@ -38,11 +41,19 @@ The Jira Cloud REST API v3 requires this for all authenticated requests.
38
41
 
39
42
  ---
40
43
 
41
- ## GitHub Token
44
+ ## Git Provider Access
42
45
 
43
46
  The spawned agent conversation clones the target repository, pushes a new branch, and
44
- opens a pull request. The GitHub token must be stored as an OpenHands secret and must
45
- have the following scopes:
47
+ opens the pull or merge request. On OpenHands Cloud and Enterprise it uses the connected
48
+ integration for the provider named by `git_provider` (GitHub, GitLab or Bitbucket). With
49
+ only an MCP connection (GitHub's with its saved token, GitLab's, or Atlassian Rovo for
50
+ Bitbucket) it commits through the tools instead, and reads a private repository through
51
+ them rather than cloning it. On a local Agent Canvas a token for the provider must be
52
+ stored as an OpenHands secret.
53
+
54
+ ### GitHub
55
+
56
+ The GitHub token must have the following scopes:
46
57
 
47
58
  | Scope | Required for |
48
59
  |---|---|
@@ -57,20 +68,39 @@ inherits the user's configured secrets automatically through the agent server.
57
68
  Navigate to **Settings → Secrets** and store the token under the name `GITHUB_TOKEN`
58
69
  (or whatever name is expected by the agent's GitHub skill).
59
70
 
71
+ ### GitLab
72
+
73
+ Store a personal access token with the `api` scope and at least the Developer role on the
74
+ target projects under the name `GITLAB_TOKEN`.
75
+
76
+ Calling GitLab MCP directly, to commit without a token, needs GitLab 19.4 or later, where its
77
+ branch, commit and repository tools first appear. On OpenHands Cloud and Enterprise the token
78
+ of the user's GitLab sign-in or connected integration reaches the conversation as
79
+ `GITLAB_TOKEN`, so on an older GitLab sign in with GitLab or connect it under Settings ->
80
+ Integrations, and store this secret only if neither is possible.
81
+
82
+ ### Bitbucket
83
+
84
+ Store the token under the name `BITBUCKET_TOKEN`; the `bitbucket` skill describes the token
85
+ type and permissions.
86
+
60
87
  ---
61
88
 
62
89
  ## config.json Field Reference
63
90
 
64
91
  | Field | Required | Default | Description |
65
92
  |---|---|---|---|
66
- | `jira_base_url` | ✅ | - | Full URL of your Atlassian instance, e.g. `https://acme.atlassian.net` |
67
- | `jira_email` | ✅ | - | Email address of the Atlassian account that owns the API token |
93
+ | `jira_mcp_server` | ❌ | - | Name the connected Atlassian Rovo MCP server is stored under. When set, the poller reaches Jira through it (OpenHands Cloud and Enterprise only) and the three fields after `jira_cloud_id` are not used |
94
+ | `jira_cloud_id` | with `jira_mcp_server` | - | Atlassian cloud id of the Jira site |
95
+ | `jira_base_url` | without `jira_mcp_server` | - | Full URL of your Atlassian instance, e.g. `https://acme.atlassian.net` |
96
+ | `jira_email` | without `jira_mcp_server` | - | Email address of the Atlassian account that owns the API token |
68
97
  | `jira_token_secret` | ❌ | `"JIRA_CLOUD_KEY"` | Name of the OpenHands secret holding the Jira API token |
69
98
  | `jira_label` | ❌ | `"create-pr"` | Jira issue label to watch for |
99
+ | `git_provider` | ❌ | `"github"` | `github`, `gitlab` or `bitbucket` - the provider the ticket names its repository on; changes only the prompt |
70
100
 
71
- > **GitHub repo**: not a config field. The target repository is read from the body of each
72
- > Jira ticket. Ticket authors must include the repo in `owner/repo` format or as a full
73
- > GitHub URL (e.g. `https://github.com/acme-org/backend`).
101
+ > **Repository**: not a config field. The target repository is read from the body of each
102
+ > Jira ticket: `owner/repo` or a GitHub URL on GitHub, `group/subgroup/project` or a full
103
+ > URL on GitLab, `workspace/repo` on bitbucket.org.
74
104
 
75
105
  ---
76
106
 
@@ -142,11 +172,12 @@ The response is `SearchAndReconcileResults`; the script reads the `issues` array
142
172
  - The Jira project has issues with the label but all are in a Done status category.
143
173
  The JQL filter is `statusCategory != Done` - reopen or create a fresh issue.
144
174
 
145
- ### Conversation started but no PR appears
175
+ ### Conversation started but no PR or MR appears
146
176
 
147
177
  - The spawned conversation inherits the user's agent/LLM settings. Check the conversation
148
178
  in the OpenHands UI (it appears in the conversations list under the automation run).
149
- - The GitHub token may lack `repo` scope to push or open PRs.
179
+ - The connected integration or token may lack the scope to push or open the request
180
+ (GitHub `repo`, GitLab `api` with the Developer role).
150
181
  - The target repo's default branch may be protected; the token must have bypass permission,
151
182
  or the PR should be created against a different base branch.
152
183
 
@@ -166,13 +197,13 @@ The response is `SearchAndReconcileResults`; the script reads the `issues` array
166
197
 
167
198
  ## Multiple Deployments
168
199
 
169
- To watch multiple Jira projects or multiple GitHub repos, deploy separate automations —
200
+ To watch multiple Jira projects or multiple repositories, deploy separate automations —
170
201
  one per `config.json`. Each automation has its own KV-store namespace (keyed by
171
202
  automation ID), so processed-key sets do not cross-contaminate.
172
203
 
173
204
  Example: two automations, one per project:
174
205
 
175
- | Automation name | `jira_base_url` | `jira_label` | GitHub repo (in ticket body) |
206
+ | Automation name | `jira_base_url` | `jira_label` | Repository (in ticket body) |
176
207
  |---|---|---|---|
177
208
  | `Jira issue-to-PR - frontend` | `https://acme.atlassian.net` | `create-pr` | `acme/frontend` (written by ticket author) |
178
209
  | `Jira issue-to-PR - backend` | `https://acme.atlassian.net` | `create-pr-backend` | `acme/backend` (written by ticket author) |
@@ -2,19 +2,26 @@
2
2
  Jira issue-to-PR poller — reads all configuration from config.json in the same directory.
3
3
 
4
4
  config.json fields:
5
- jira_base_url e.g. "https://yourcompany.atlassian.net"
6
- jira_email Atlassian account email used for Basic auth
5
+ jira_mcp_server Settings name of the connected Atlassian Rovo MCP server. When set,
6
+ Jira is reached through it (OpenHands Cloud and Enterprise only)
7
+ and no Jira credential is configured here
8
+ jira_cloud_id Atlassian cloud id of the Jira site; required with jira_mcp_server
9
+ jira_base_url e.g. "https://yourcompany.atlassian.net"; without jira_mcp_server
10
+ jira_email Atlassian account email used for Basic auth; without jira_mcp_server
7
11
  jira_token_secret Name of the OpenHands secret holding the Jira API token
8
12
  jira_label Label to watch for (default: "create-pr")
9
13
  max_new_per_run Max conversations dispatched per run (default: 5)
14
+ git_provider Where the ticket's repository lives: "github" (default), "gitlab"
15
+ or "bitbucket". It only changes what the conversation is asked
10
16
 
11
- The target GitHub repository is NOT configured here. Each Jira ticket body must include
12
- the repo in "owner/repo" format; the spawned agent extracts it from the ticket text.
17
+ The target repository is NOT configured here. Each Jira ticket body must name it
18
+ (owner/repo on GitHub, group/project or a URL on GitLab, workspace/repo on Bitbucket);
19
+ the spawned agent extracts it from the ticket text.
13
20
 
14
21
  Runs against a local agent server (AGENT_SERVER_URL is injected) or on OpenHands Cloud
15
22
  (OPENHANDS_CLOUD_API_URL / OPENHANDS_API_KEY / SANDBOX_ID are injected instead).
16
23
  """
17
- import base64, json, os, re, sys, tempfile, urllib.error, urllib.request, uuid
24
+ import base64, json, os, re, sys, tempfile, urllib.error, urllib.parse, urllib.request, uuid
18
25
  from datetime import datetime, timezone
19
26
  from pathlib import Path
20
27
 
@@ -23,11 +30,67 @@ _HERE = Path(__file__).parent
23
30
  with open(_HERE / "config.json") as _f:
24
31
  _cfg = json.load(_f)
25
32
 
26
- JIRA_BASE_URL = _cfg["jira_base_url"].rstrip("/")
27
- JIRA_EMAIL = _cfg["jira_email"]
33
+ # Jira is reached through this connected MCP server when it is named, and through
34
+ # Jira's REST API with an API token otherwise.
35
+ JIRA_MCP_SERVER = _cfg.get("jira_mcp_server")
36
+ JIRA_CLOUD_ID = _cfg["jira_cloud_id"] if JIRA_MCP_SERVER else None
37
+ JIRA_BASE_URL = None if JIRA_MCP_SERVER else _cfg["jira_base_url"].rstrip("/")
38
+ JIRA_EMAIL = None if JIRA_MCP_SERVER else _cfg["jira_email"]
28
39
  JIRA_TOKEN_SECRET = _cfg.get("jira_token_secret", "JIRA_CLOUD_KEY")
29
40
  JIRA_LABEL = _cfg.get("jira_label", "create-pr")
41
+ # Atlassian renamed the comment tool in v2 of its MCP server and moves v1 connections
42
+ # to the v2 tools on its own schedule, so a deployment can meet either name.
43
+ JIRA_COMMENT_TOOLS = ("addOrEditJiraIssueComment", "addCommentToJiraIssue")
30
44
  MAX_NEW_PER_RUN = int(_cfg.get("max_new_per_run", 5))
45
+ GIT_PROVIDER = _cfg.get("git_provider", "github")
46
+ # What the conversation is told about the provider the ticket names its repository on.
47
+ # Only the prompt differs: the conversation pushes with the user's connected integration,
48
+ # and without one it delivers through the tools of the connected MCP server instead.
49
+ GIT_PROVIDERS = {
50
+ "github": {
51
+ "name": "GitHub",
52
+ "request": "Pull Request",
53
+ "short": "PR",
54
+ "reference": '"owner/repo" format (e.g. "acme-org/backend") or a full GitHub URL\n'
55
+ ' (e.g. "https://github.com/acme-org/backend")',
56
+ "clone": "https://github.com/<owner>/<repo>",
57
+ "delivery": "",
58
+ },
59
+ "gitlab": {
60
+ "name": "GitLab",
61
+ "request": "Merge Request",
62
+ "short": "MR",
63
+ "reference": '"group/project" format, subgroups included (e.g. "acme-org/platform/backend"),\n'
64
+ ' or a full GitLab URL, which may be on a self-managed host\n'
65
+ ' (e.g. "https://gitlab.example.com/acme-org/platform/backend")',
66
+ "clone": "https://gitlab.com/<group>/<project>, or the host the ticket's URL names",
67
+ "delivery": """
68
+ Delivering through GitLab:
69
+ - If GitLab tools from a connected MCP server are available to you, prefer them for reading
70
+ the issue and for opening the merge request.
71
+ - Without a `GITLAB_TOKEN` you cannot push with git: create the branch with `add_branch`,
72
+ commit your changes with `add_commit` (one call, a file action per changed file), and open
73
+ the merge request with `save_merge_request`. A private project cannot be cloned without a
74
+ credential; read it with `list_repository_tree` and `get_repository_file` instead.
75
+ """,
76
+ },
77
+ "bitbucket": {
78
+ "name": "Bitbucket",
79
+ "request": "Pull Request",
80
+ "short": "PR",
81
+ "reference": '"workspace/repo" format (e.g. "acme-org/backend") or a full Bitbucket URL\n'
82
+ ' (e.g. "https://bitbucket.org/acme-org/backend")',
83
+ "clone": "https://bitbucket.org/<workspace>/<repo>",
84
+ "delivery": """
85
+ Delivering through Bitbucket:
86
+ - Without a `BITBUCKET_TOKEN`, use the Bitbucket tools of the connected Atlassian Rovo MCP
87
+ server: read the repository with `getBitbucketRepoFileContent`, create the branch with
88
+ `createBitbucketRepoBranch`, commit your changes with `createBitbucketRepoCommit`, and open
89
+ the pull request with `createBitbucketRepoPullRequest`. Tools the server does not list
90
+ directly are reached through `discover` and `executeRead` / `executeWrite`.
91
+ """,
92
+ },
93
+ }
31
94
  # How often a conversation that OpenHands Cloud failed to start is started again.
32
95
  MAX_START_ATTEMPTS = 3
33
96
 
@@ -163,14 +226,39 @@ def extract_adf_text(adf):
163
226
  return " ".join(p for p in parts if p).strip()
164
227
 
165
228
 
229
+ def call_mcp_tool(name, arguments):
230
+ """Call a tool on the connected MCP server and return its text output.
231
+
232
+ OpenHands makes the call with the credentials the user stored when connecting
233
+ the server, so this run holds no Jira credential of its own.
234
+ """
235
+ server = urllib.parse.quote(JIRA_MCP_SERVER, safe="")
236
+ result = _cloud_request("POST", f"/api/v1/mcp/servers/{server}/call-tool", {
237
+ "name": name, "arguments": arguments,
238
+ })
239
+ if result.get("is_error"):
240
+ raise RuntimeError(f"{name} failed: {str(result.get('text'))[:500]}")
241
+ return result.get("text") or ""
242
+
243
+
166
244
  def fetch_labeled_issues(auth_header):
167
- """Return open Jira issues with JIRA_LABEL using the current v3 search endpoint."""
168
- url = f"{JIRA_BASE_URL}/rest/api/3/search/jql"
169
- body = json.dumps({
245
+ """Return open Jira issues with JIRA_LABEL.
246
+
247
+ Uses the connected MCP server's JQL search, or the current v3 search endpoint.
248
+ """
249
+ search = {
170
250
  "jql": f'labels = "{JIRA_LABEL}" AND statusCategory != Done',
171
251
  "fields": ["key", "summary", "description", "status", "updated"],
172
252
  "maxResults": 50,
173
- }).encode()
253
+ }
254
+ if JIRA_MCP_SERVER:
255
+ text = call_mcp_tool("searchJiraIssuesUsingJql", {"cloudId": JIRA_CLOUD_ID, **search})
256
+ try:
257
+ return json.loads(text).get("issues", [])
258
+ except (ValueError, AttributeError) as exc:
259
+ raise RuntimeError(f"Unexpected Jira search result: {text[:500]}") from exc
260
+ url = f"{JIRA_BASE_URL}/rest/api/3/search/jql"
261
+ body = json.dumps(search).encode()
174
262
  req = urllib.request.Request(
175
263
  url, data=body,
176
264
  headers={
@@ -188,7 +276,19 @@ def fetch_labeled_issues(auth_header):
188
276
 
189
277
 
190
278
  def post_jira_comment(issue_key, auth_header, text):
191
- """Post a plain-text comment on a Jira issue using ADF."""
279
+ """Post a plain-text comment on a Jira issue."""
280
+ if JIRA_MCP_SERVER:
281
+ errors = []
282
+ for tool in JIRA_COMMENT_TOOLS:
283
+ try:
284
+ call_mcp_tool(tool, {
285
+ "cloudId": JIRA_CLOUD_ID, "issueIdOrKey": issue_key, "commentBody": text,
286
+ })
287
+ return
288
+ except RuntimeError as exc:
289
+ errors.append(str(exc))
290
+ print(f"Warning: failed to post Jira comment on {issue_key}: {'; '.join(errors)}")
291
+ return
192
292
  url = f"{JIRA_BASE_URL}/rest/api/3/issue/{issue_key}/comment"
193
293
  body = json.dumps({
194
294
  "body": {
@@ -321,10 +421,21 @@ try:
321
421
  raise RuntimeError("The automation KV store is required on OpenHands Cloud, "
322
422
  "but AUTOMATION_KV_TOKEN is not set for this run.")
323
423
 
324
- jira_token = get_secret(JIRA_TOKEN_SECRET)
325
- auth_header = "Basic " + base64.b64encode(
326
- f"{JIRA_EMAIL}:{jira_token}".encode()
327
- ).decode()
424
+ if GIT_PROVIDER not in GIT_PROVIDERS:
425
+ raise RuntimeError(f"Unknown git_provider {GIT_PROVIDER!r}; "
426
+ f"expected one of: {', '.join(GIT_PROVIDERS)}")
427
+ git = GIT_PROVIDERS[GIT_PROVIDER]
428
+
429
+ auth_header = None
430
+ if not JIRA_MCP_SERVER:
431
+ jira_token = get_secret(JIRA_TOKEN_SECRET)
432
+ auth_header = "Basic " + base64.b64encode(
433
+ f"{JIRA_EMAIL}:{jira_token}".encode()
434
+ ).decode()
435
+ elif IS_LOCAL:
436
+ # Only OpenHands Cloud and Enterprise call a connected MCP server for a run.
437
+ raise RuntimeError("jira_mcp_server needs OpenHands Cloud or Enterprise; "
438
+ "a local run reaches Jira with jira_token_secret.")
328
439
 
329
440
  state = load_state()
330
441
  processed_keys = set(state.get("processed_keys", []))
@@ -343,7 +454,7 @@ try:
343
454
  if not IS_LOCAL:
344
455
  settle_cloud_starts(state, processed_keys, auth_header)
345
456
 
346
- print(f"Polling {JIRA_BASE_URL} for issues labeled '{JIRA_LABEL}'…")
457
+ print(f"Polling {JIRA_BASE_URL or JIRA_MCP_SERVER} for issues labeled '{JIRA_LABEL}'…")
347
458
  issues = fetch_labeled_issues(auth_header)
348
459
  print(f"Total matching issues : {len(issues)}")
349
460
 
@@ -393,28 +504,27 @@ try:
393
504
  description = extract_adf_text(issue["fields"].get("description"))
394
505
  branch = f"jira/{key.lower()}"
395
506
 
396
- prompt = f"""Create a GitHub Pull Request for the following Jira issue.
507
+ prompt = f"""Create a {git['name']} {git['request']} for the following Jira issue.
397
508
 
398
509
  Jira Issue : {key}
399
510
  Summary : {summary}
400
511
  Description: {description or "No description provided."}
401
512
 
402
513
  Steps:
403
- 1. Find the target GitHub repository in the Description above. Look for a reference in
404
- "owner/repo" format (e.g. "acme-org/backend") or a full GitHub URL
405
- (e.g. "https://github.com/acme-org/backend"). Use that repository.
514
+ 1. Find the target {git['name']} repository in the Description above. Look for a reference in
515
+ {git['reference']}. Use that repository.
406
516
  If no repository is mentioned, create a file `jira/{key}/notes.md` with the issue
407
- details and print a message explaining that no GitHub repo was found in the ticket.
408
- 2. Clone the repository (e.g. https://github.com/<owner>/<repo>).
517
+ details and print a message explaining that no {git['name']} repo was found in the ticket.
518
+ 2. Clone the repository (e.g. {git['clone']}).
409
519
  3. Create branch `{branch}` from the default branch.
410
520
  4. Implement the changes described in the issue.
411
521
  If the description is vague or missing, create `jira/{key}/notes.md`
412
522
  with the issue key, summary, and description as a placeholder.
413
- 5. Commit, push the branch, and open a Pull Request:
523
+ 5. Commit, push the branch, and open a {git['request']}:
414
524
  - Title : [{key}] {summary}
415
525
  - Body : Reference the Jira issue key and describe the changes made.
416
- 6. Print the PR URL when done.
417
- """
526
+ 6. Print the {git['short']} URL when done.
527
+ {git['delivery']}"""
418
528
  if IS_LOCAL:
419
529
  workdir = tempfile.mkdtemp(prefix=f"jira-{key.lower()}-")
420
530
  payload = {
@@ -37,12 +37,21 @@ This skill is activated by keywords:
37
37
  thread immediately on trigger detection
38
38
  - **Automatic summaries**: when the conversation reaches a terminal state the
39
39
  agent's final response is posted back to the thread; error/stuck states
40
- receive a clear error notice
40
+ receive a clear error notice, and every result shows the LLM settings source
41
+ and concrete model
41
42
  - **Persistent state**: conversation tracking and poll timestamps are stored
42
- in `automation-state/slack_poller_{automation_id}.json` across runs
43
+ in the automation service's KV store across runs (a local Agent Canvas
44
+ without one uses `automation-state/slack_poller_{automation_id}.json`)
45
+ - **Runs anywhere**: on a local Agent Canvas, and on OpenHands Cloud and
46
+ Enterprise, where each conversation is started through the OpenHands API in
47
+ a sandbox of its own with the user's own settings
43
48
 
44
49
  ## Prerequisites
45
50
 
51
+ The footer reports `default` and the concrete model from the server settings
52
+ already used to start the conversation. Follow-up replies keep those recorded
53
+ values because they reuse the same conversation.
54
+
46
55
  Set at least one of the following in **OpenHands Settings - Secrets**:
47
56
 
48
57
  | Secret | Token type | Minimum scopes |
@@ -54,7 +63,7 @@ Optional:
54
63
 
55
64
  | Secret | Default | Purpose |
56
65
  |--------|---------|---------|
57
- | `OPENHANDS_URL` | `http://localhost:8000` | Base URL for conversation links posted in Slack |
66
+ | `OPENHANDS_URL` | `http://localhost:8000` | Base URL for conversation links posted in Slack (local Agent Canvas only) |
58
67
 
59
68
  ## Quick Start
60
69