From 7fc16777a603e093855c273a42fe94b91dbc7074 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Thu, 16 Jul 2026 11:34:55 +0300 Subject: [PATCH] feat(jira-workflow): add GitLab create-mr.sh helper - creates a merge request via GitLab API (curl + GITLAB_TOKEN, PRIVATE-TOKEN header) - derives host + project path from git remote origin (or GITLAB_HOST); no hardcoded URLs - refuses to open an MR from a protected source branch; MR_PUSH/MR_DRY_RUN toggles - optionally attaches the MR link to a Jira ticket via link-mr.sh - verified parsing for ssh (scp-form) and https remotes incl. nested namespaces --- skills/jira-workflow/SKILL.md | 13 +++ skills/jira-workflow/scripts/create-mr.sh | 112 ++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100755 skills/jira-workflow/scripts/create-mr.sh diff --git a/skills/jira-workflow/SKILL.md b/skills/jira-workflow/SKILL.md index c677923..a0c9d87 100644 --- a/skills/jira-workflow/SKILL.md +++ b/skills/jira-workflow/SKILL.md @@ -17,6 +17,9 @@ description: Используй, когда нужно работать с ко - **Токен доступа** — из переменной окружения `JIRA_TOKEN` (personal access token). - Токен в команды и логи не подставляй в открытом виде — скрипты читают его из окружения сами. +Для создания merge request в GitLab дополнительно нужен `GITLAB_TOKEN` (токен с scope `api`); +хост и путь проекта скрипт определяет из `git remote origin` (или `GITLAB_HOST`) — адреса не хардкодятся. + Быстрая проверка окружения: ```bash @@ -32,6 +35,9 @@ description: Используй, когда нужно работать с ко - `./scripts/add-comment.sh "<текст>"` — добавить комментарий. - `./scripts/link-mr.sh "" ["<заголовок>"]` — приложить ссылку на MR (добавляется комментарием со ссылкой; при доступном remote link API — см. TODO в скрипте). +- `./scripts/create-mr.sh "" [target] [source] [jira-key]` — создать merge request в + GitLab (по текущей ветке), вывести его URL и, если передан `jira-key` и заданы + `TRACKER_URL`/`JIRA_TOKEN`, автоматически приложить ссылку к тикету. Пример: @@ -39,8 +45,15 @@ description: Используй, когда нужно работать с ко ./scripts/get-issue.sh PROJ-123 ./scripts/add-comment.sh PROJ-123 "Начал работу, воспроизвёл баг" ./scripts/link-mr.sh PROJ-123 "https://<git-host>/ai/foo/-/merge_requests/42" "Fix PROJ-123" + +# создать MR из текущей ветки в main и сразу привязать к тикету: +GITLAB_TOKEN=... ./scripts/create-mr.sh "Fix PROJ-123: NPE" main "" PROJ-123 ``` +Флаги окружения `create-mr.sh`: `GITLAB_TOKEN` (обязателен), `GITLAB_HOST` (override хоста), +`MR_PUSH=0` (не пушить ветку), `MR_DRY_RUN=1` (показать запрос без вызова API). +MR из защищённой ветки (`main`/`master`/`release/*`) скрипт создавать откажется. + ## Соглашения <!-- TODO: заполнить реальными значениями компании --> diff --git a/skills/jira-workflow/scripts/create-mr.sh b/skills/jira-workflow/scripts/create-mr.sh new file mode 100755 index 0000000..613e1c2 --- /dev/null +++ b/skills/jira-workflow/scripts/create-mr.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# create-mr.sh "<title>" [target-branch] [source-branch] [jira-key] +# +# Creates a GitLab merge request for the current repository via the GitLab API. +# Host and project path are derived from `git remote origin` (or GITLAB_HOST), +# so no URLs are hardcoded. If a Jira key is given and TRACKER_URL/JIRA_TOKEN are +# set, the MR link is also attached to the ticket via link-mr.sh. +# +# Env: +# GITLAB_TOKEN (required) GitLab personal/project access token with api scope +# GITLAB_HOST (optional) override host, e.g. git.codelab.vc +# MR_PUSH (optional) 0 to skip pushing the source branch (default: push) +# MR_DRY_RUN (optional) 1 to print the request and exit without calling the API +# +# Examples: +# ./create-mr.sh "Fix PROJ-123: null pointer" main +# ./create-mr.sh "Add feature X" main feature/PROJ-42-x PROJ-42 +set -euo pipefail + +TITLE="${1:?usage: create-mr.sh \"<title>\" [target-branch] [source-branch] [jira-key]}" +TARGET="${2:-main}" +SOURCE="${3:-$(git rev-parse --abbrev-ref HEAD)}" +JIRA_KEY="${4:-}" + +: "${GITLAB_TOKEN:?set GITLAB_TOKEN (GitLab access token with api scope)}" + +# --- Refuse to open an MR *from* a protected branch (safety). ---------------- +case "$SOURCE" in + main | master | release/*) + echo "Refusing: source branch '$SOURCE' looks protected. Create a feature branch first." >&2 + exit 1 + ;; +esac + +# --- Derive host and project path from the origin remote. ------------------- +URL="$(git config --get remote.origin.url)" +URL="${URL%.git}" +case "$URL" in + *://*) # scheme://[user@]host[:port]/path + REST="${URL#*://}" + REST="${REST#*@}" + HOST_PORT="${REST%%/*}" + PATH_NS="${REST#*/}" + HOST="${HOST_PORT%%:*}" + ;; + *@*:*) # scp-like: user@host:path + REST="${URL#*@}" + HOST="${REST%%:*}" + PATH_NS="${REST#*:}" + ;; + *) + echo "Cannot parse origin remote URL: $URL" >&2 + exit 1 + ;; +esac + +HOST="${GITLAB_HOST:-$HOST}" +if [ -z "$HOST" ] || [ -z "$PATH_NS" ]; then + echo "Could not determine GitLab host/project from origin ($URL). Set GITLAB_HOST." >&2 + exit 1 +fi + +# URL-encode the project path (only '/' needs encoding for a namespaced path). +PROJECT_ENC="${PATH_NS//\//%2F}" +API="https://${HOST}/api/v4/projects/${PROJECT_ENC}/merge_requests" + +# --- Build request body (jq escapes; fall back to naive JSON). -------------- +if command -v jq >/dev/null 2>&1; then + BODY="$(jq -n \ + --arg s "$SOURCE" --arg t "$TARGET" --arg title "$TITLE" \ + '{source_branch:$s, target_branch:$t, title:$title, remove_source_branch:true, squash:true}')" +else + BODY="{\"source_branch\":\"${SOURCE}\",\"target_branch\":\"${TARGET}\",\"title\":\"${TITLE//\"/\\\"}\",\"remove_source_branch\":true,\"squash\":true}" +fi + +if [ "${MR_DRY_RUN:-0}" = "1" ]; then + echo "DRY RUN" + echo "POST ${API}" + echo "body: ${BODY}" + echo "would push: $([ "${MR_PUSH:-1}" = "1" ] && echo "git push -u origin ${SOURCE}" || echo "(skipped)")" + exit 0 +fi + +# --- Ensure the source branch exists on the remote. ------------------------- +if [ "${MR_PUSH:-1}" = "1" ]; then + git push -u origin "$SOURCE" +fi + +# --- Create the MR. --------------------------------------------------------- +RESPONSE="$(curl -fsS -X POST \ + -H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$BODY" "$API")" + +if command -v jq >/dev/null 2>&1; then + WEB_URL="$(printf '%s' "$RESPONSE" | jq -r '.web_url // empty')" +else + WEB_URL="$(printf '%s' "$RESPONSE" | sed -n 's/.*"web_url":"\([^"]*\)".*/\1/p' | head -1)" +fi + +if [ -z "$WEB_URL" ]; then + echo "MR request sent, but could not parse web_url from response:" >&2 + echo "$RESPONSE" >&2 + exit 1 +fi + +echo "MR created: $WEB_URL" + +# --- Optionally attach the MR link to the Jira ticket. ---------------------- +if [ -n "$JIRA_KEY" ] && [ -n "${TRACKER_URL:-}" ] && [ -n "${JIRA_TOKEN:-}" ]; then + "$(dirname "$0")/link-mr.sh" "$JIRA_KEY" "$WEB_URL" "$TITLE" +fi