Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The JS and Python versions are kept in lock-step by `release-please` and guarded
## Extensions Catalog

<!-- BEGIN AUTO-GENERATED CATALOG -->
This repository contains **2 marketplace(s)** with **68 extensions** (58 skills, 10 plugins).
This repository contains **2 marketplace(s)** with **69 extensions** (59 skills, 10 plugins).

### large-codebase

Expand All @@ -115,7 +115,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases

Official skills and plugins for OpenHands — the open-source AI software engineer.

**64 extensions** (56 skills, 8 plugins)
**65 extensions** (57 skills, 8 plugins)

| Name | Type | Description | Commands |
|------|------|-------------|----------|
Expand Down Expand Up @@ -146,6 +146,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
| github-pr-reviewer | skill | Create an automation that reviews GitHub pull requests when they are opened or updated. Inspects the diff, changed fi... | `/pr-reviewer:setup` |
| github-repo-monitor | skill | Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger p... | `/github-monitor:poll` |
| gitlab | skill | Interact with GitLab repositories, merge requests, and APIs using the GITLAB_TOKEN environment variable. Use when wor... | — |
| gitlab-issue-to-mr | skill | Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default b... | `/issue-to-mr:setup` |
| incident-retrospective | skill | Create an automation that drafts incident retrospectives by gathering incident-channel messages from Slack, collectin... | `/incident-retro:setup` |
| iterate | skill | Iterate on a GitHub pull request — drive it through CI, code review, and QA until merge-ready. Monitors state, fixes ... | `/iterate`, `/verify`, `/babysit` |
| jira-issue-to-pr | skill | Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable lab... | — |
Expand Down
3 changes: 3 additions & 0 deletions automations/bundle-index.js

Large diffs are not rendered by default.

24 changes: 13 additions & 11 deletions automations/catalog-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ import entry3 from "./catalog/slack-standup-digest/manifest.json" with { type: "
import entry4 from "./catalog/slack-channel-monitor/manifest.json" with { type: "json" };
import entry5 from "./catalog/linear-triage-assistant/manifest.json" with { type: "json" };
import entry6 from "./catalog/linear-issue-to-github-pr/manifest.json" with { type: "json" };
import entry7 from "./catalog/linear-issue-to-gitlab-mr/manifest.json" with { type: "json" };
import entry8 from "./catalog/linear-issue-to-bitbucket-pr/manifest.json" with { type: "json" };
import entry9 from "./catalog/jira-issue-to-pr/manifest.json" with { type: "json" };
import entry10 from "./catalog/qa-changes/manifest.json" with { type: "json" };
import entry11 from "./catalog/jira-issue-to-gitlab-mr/manifest.json" with { type: "json" };
import entry12 from "./catalog/research-brief-writer/manifest.json" with { type: "json" };
import entry13 from "./catalog/jira-issue-to-bitbucket-pr/manifest.json" with { type: "json" };
import entry14 from "./catalog/github-agents-md-maintainer/manifest.json" with { type: "json" };
import entry15 from "./catalog/upstream-fork-sync/manifest.json" with { type: "json" };
import entry16 from "./catalog/incident-retrospective-drafter/manifest.json" with { type: "json" };
import entry17 from "./catalog/news-digest/manifest.json" with { type: "json" };
import entry7 from "./catalog/gitlab-issue-to-mr/manifest.json" with { type: "json" };
import entry8 from "./catalog/linear-issue-to-gitlab-mr/manifest.json" with { type: "json" };
import entry9 from "./catalog/linear-issue-to-bitbucket-pr/manifest.json" with { type: "json" };
import entry10 from "./catalog/jira-issue-to-pr/manifest.json" with { type: "json" };
import entry11 from "./catalog/qa-changes/manifest.json" with { type: "json" };
import entry12 from "./catalog/jira-issue-to-gitlab-mr/manifest.json" with { type: "json" };
import entry13 from "./catalog/research-brief-writer/manifest.json" with { type: "json" };
import entry14 from "./catalog/jira-issue-to-bitbucket-pr/manifest.json" with { type: "json" };
import entry15 from "./catalog/github-agents-md-maintainer/manifest.json" with { type: "json" };
import entry16 from "./catalog/upstream-fork-sync/manifest.json" with { type: "json" };
import entry17 from "./catalog/incident-retrospective-drafter/manifest.json" with { type: "json" };
import entry18 from "./catalog/news-digest/manifest.json" with { type: "json" };

export const AUTOMATION_CATALOG_ENTRIES = [
entry0,
Expand All @@ -40,4 +41,5 @@ export const AUTOMATION_CATALOG_ENTRIES = [
entry15,
entry16,
entry17,
entry18,
];
124 changes: 124 additions & 0 deletions automations/catalog/gitlab-issue-to-mr/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
{
"id": "gitlab-issue-to-mr",
"name": "GitLab issue to MR",
"category": "Software development",
"description": "Watch for a configurable label on GitLab issues, implement the issue in a clone of the default branch, and open a merge request for each label event.",
"requires": {
"integrations": {
"gitlab": {
"message": "Used to read labelled issues, push the branch, and open the merge request."
}
},
"features": [
"customTarball"
]
},
"popularityRank": 88,
"estimatedSetupMinutes": 4,
"exampleImplementation": "Trigger: cron polling for open GitLab issues with a configured label such as openhands\nRequired secret: GITLAB_TOKEN, a personal or project access token with the api scope and at least the Developer role\n\n1. Read the projects, trigger label, branch prefix, merge request mode, GitLab API URL, and polling schedule from setup.\n2. Poll each project independently, with its own state, so issue IIDs never collide.\n3. List open labelled issues and find the latest matching GitLab label resource event for each.\n4. Deduplicate on the label event ID so every label application queues exactly one attempt.\n5. Clone the default branch into a directory of its own, create the working branch, and start an OpenHands conversation with that directory as its workspace. The clone carries no credential and the agent is handed no secrets, because the prompt is built from an issue body that anyone can write.\n6. Comment on the issue with the branch and the conversation link.\n7. Once the conversation has stopped, commit whatever the agent left, push the branch, open a merge request titled after the issue and prefixed with Draft:, and comment the link on the issue. An agent that made no changes gets its answer posted instead.\n8. Remove the clone once the conversation has stopped, so nothing accumulates between runs.",
"impact": {
"basis": "completed-runs",
"one": "1 issue sweep completed",
"other": "{{count}} issue sweeps completed"
},
"setup": {
"version": "1.0",
"mode": "direct",
"form": {
"triggers": {
"cron": {
"schedule": {
"type": "cron",
"label": "Check frequency",
"help": "How often to look for newly labelled issues.",
"default": "*/15 * * * *",
"required": true
},
"timezone": {
"type": "timezone",
"label": "Timezone",
"help": "Timezone the schedule is interpreted in.",
"default": "UTC",
"required": true
}
}
},
"args": {
"projects": {
"type": "repo-picker",
"label": "Projects",
"help": "The GitLab projects whose labelled issues are implemented. Each is polled independently and keeps its own state, so issue numbers never collide between them.",
"provider": "gitlab",
"multiple": true,
"required": true
},
"triggerLabel": {
"type": "text",
"label": "Trigger label",
"help": "Only issues carrying this label are worked on.",
"default": "openhands",
"required": true,
"constraints": {
"minLength": 1,
"maxLength": 50
}
},
"branchPrefix": {
"type": "text",
"label": "Branch prefix",
"help": "Branches are named after this prefix and the issue number, such as openhands/issue-42.",
"default": "openhands/issue",
"required": true,
"constraints": {
"minLength": 1,
"maxLength": 50
}
},
"mergeRequestMode": {
"type": "select",
"label": "Merge request mode",
"help": "Whether the merge request is opened as a draft or ready for review. GitLab marks a draft by prefixing its title with Draft:.",
"default": "draft",
"required": true,
"options": [
{
"value": "draft",
"label": "Draft"
},
{
"value": "ready",
"label": "Ready for review"
}
]
},
"gitlabApiUrl": {
"type": "text",
"label": "GitLab API URL",
"help": "The API root of the GitLab instance. Change it for a self-managed instance, such as https://gitlab.example.com/api/v4.",
"default": "https://gitlab.com/api/v4",
"required": true,
"constraints": {
"minLength": 1,
"maxLength": 200
}
}
}
},
"bundle": {
"version": "1.0.0",
"entrypoint": "python3 main.py",
"timeout": 900,
"files": {
"main.py": "skills/gitlab-issue-to-mr/scripts/main.py"
},
"config": {
"projects": "{{form.projects}}",
"trigger_label": "{{form.triggerLabel}}",
"branch_prefix": "{{form.branchPrefix}}",
"merge_request_mode": "{{form.mergeRequestMode}}",
"gitlab_api_url": "{{form.gitlabApiUrl}}"
}
},
"message": "This deployment cannot run the scheduled issue-to-MR automation directly. Set it up in this conversation instead: confirm the GitLab projects to watch, the trigger label, the branch prefix, whether merge requests open as drafts, the GitLab API URL, and the polling schedule, then create the automation."
}
}
13 changes: 13 additions & 0 deletions marketplaces/openhands-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,19 @@
"integration"
]
},
{
"name": "gitlab-issue-to-mr",
"source": "./skills/gitlab-issue-to-mr",
"description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default branch for the agent, then commits, pushes, and opens the merge request itself - the agent is handed no push credential.",
"category": "automations",
"keywords": [
"gitlab",
"issue",
"merge-request",
"automation",
"integration"
]
},
{
"name": "incident-retrospective",
"source": "./skills/incident-retrospective",
Expand Down
1 change: 1 addition & 0 deletions skills/gitlab-issue-to-mr/.claude-plugin
1 change: 1 addition & 0 deletions skills/gitlab-issue-to-mr/.codex-plugin
19 changes: 19 additions & 0 deletions skills/gitlab-issue-to-mr/.plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "gitlab-issue-to-mr",
"version": "1.0.0",
"description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. It clones the default branch, starts one OpenHands conversation per label event, then commits, pushes, and opens the merge request itself, without ever handing the agent a push credential.",
"author": {
"name": "OpenHands",
"email": "contact@all-hands.dev"
},
"homepage": "https://github.com/OpenHands/extensions",
"repository": "https://github.com/OpenHands/extensions",
"license": "MIT",
"keywords": [
"gitlab",
"issue",
"merge-request",
"automation",
"integration"
]
}
92 changes: 92 additions & 0 deletions skills/gitlab-issue-to-mr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# GitLab Issue to MR

Create an automation that implements GitLab issues when a configurable trigger
label is applied, and opens the merge request for you.

## Trigger

This skill is activated by:

- `/issue-to-mr:setup`

## Features

- Implements an issue on demand by watching for a GitLab label event
- Watches several projects from a single automation, each with its own state
- Works against gitlab.com and self-managed instances, subgroups included
- Processes each label application exactly once, with persistent state
- Re-runs on demand by removing and re-applying the label, on a fresh branch
- Clones the default branch for the agent, and removes the clone when the task
ends, so nothing accumulates between runs
- Lets the agent open the merge request, and opens it in Python when the agent
did not
- Opens draft merge requests by default, titled `Draft: [#42] <issue title>`,
with the agent's summary and `Closes #42` in the description
- Comments on the issue when work starts, when it finishes, and when it does not
- Posts the agent's answer instead of a merge request when it made no changes
- Caps how many conversations one poll starts, so a labelled backlog does not
start dozens at once

## What the agent is told, and what it can reach

The prompt names the project, the issue IID and title, and its URL. The agent
fetches the description, the discussion, and anything they link to itself - a
copy pasted at dispatch would already be stale, and it would stop where the
issue's own text stops.

Reading that needs credentials, so the conversation is handed one secret,
`GITLAB_TOKEN`. `AGENT_SECRET_NAMES` is an allow-list, so the rest of the secret
store stays out of reach of a conversation whose instructions came from an issue.
Set it to `[]` for public projects if you would rather it held nothing.

The deployment's MCP servers are forwarded whole, as `github-pr-reviewer` does,
so a connected GitLab server gives the agent typed tools instead of curl. That
cuts both ways: everything reachable through those servers is reachable from a
prompt written by whoever opened the issue. Connect only servers you are willing
to have driven by untrusted text.

The agent commits, pushes its branch, and opens the merge request itself, so it
appears as soon as the agent stops instead of waiting for the next poll. The
script verifies that on GitLab rather than trusting the agent's word, and opens
the merge request itself when the agent did not - a failed push or a dead
conversation never loses the work. `origin` carries no credential, so each GitLab
command has to name `GITLAB_TOKEN`, which the SDK injects only into a command
that mentions it and masks in the output.

## Two setup paths

The `/issue-to-mr:setup` conversation substitutes the constants at the top of
`scripts/main.py` and uploads the result. The catalog entry
(`automations/catalog/gitlab-issue-to-mr/`) ships the same script unmodified as
a **bundle** and renders a `config.json` beside it from the setup form, which the
script loads over those constants. Both paths produce the same automation: a
tarball the automation service runs on a cron, not a prompt handed to an agent.

## Prerequisites

Set `GITLAB_TOKEN` in OpenHands Settings -> Secrets. It needs the `api` scope and
at least the **Developer** role on every watched project - Developer is the
lowest role that can push a branch and open a merge request. Leave the configured
branch prefix out of any protected-branch rule, or the push is rejected.

For a self-managed instance, give its API root
(`https://gitlab.example.com/api/v4`) during setup.

The automation runtime must have `git` available; the script clones, commits, and
pushes with it.

## Quick Start

Ask OpenHands:

> "Set up an issue-to-MR automation for my `myorg/backend` GitLab project using
> the `openhands` label."

After setup, apply the configured label to an issue to queue an implementation.
To ask for another attempt later, remove and re-apply the label.

## See Also

- [SKILL.md](SKILL.md) - Full setup workflow reference
- [references/state-schema.md](references/state-schema.md) - State document and
task lifecycle
Loading
Loading