بازگشت به نمونه‌کارهادواپس

اتصال دوطرفه‌ی Jira Software و GitLab سازمانی

تیم توسعه‌ی یک سازمان نرم‌افزاری با GitLab سازمانی

اتصال دوطرفه‌ی Jira Software و GitLab سازمانی
چالش

کار تیم توسعه بین Jira Software و GitLab سازمانی تقسیم شده بود: یک کار دو بار ثبت می‌شد، وضعیت‌ها در دو طرف با هم نمی‌خواند و معلوم نبود هر تسک به کدام ریپو مربوط است.

راهکار

بدون خرید پلاگین، با ScriptRunner (Script REST Endpoint و Behaviours) و دو قانون Jira Automation؛ و یک قرارداد ساده: عنوان ایشو در گیت‌لب همیشه با کلید جیرا و / شروع می‌شود.

نتیجه

کاربر ریپوی واقعی را از فرم ایشو انتخاب می‌کند، ایشو خودکار در همان ریپوی گیت‌لب ساخته می‌شود و تغییرات گیت‌لب (مثل بسته شدن ایشو) به جیرا برمی‌گردد.

در خیلی از سازمان‌ها کار تیم توسعه در دو دنیای جدا اتفاق می‌افتد: برنامه‌ریزی، اولویت‌بندی و گزارش‌گیری در Jira Software، و کد، ایشوهای فنی و Merge Requestها در GitLab سازمانی که روی سرورهای خود شرکت نصب شده است. نتیجه معمولاً آشناست: یک کار دو بار ثبت می‌شود، وضعیتِ «تمام‌شده» در یک طرف هست و در طرف دیگر نیست، و وقتی کسی می‌پرسد «این تسک مربوط به کدام ریپو است؟» باید از توسعه‌دهنده سؤال کرد.

در این پروژه هدف این بود که این دو دنیا بدون خرید پلاگین اضافه به هم وصل شوند؛ فقط با ابزارهایی که از قبل روی جیرا داشتیم: ScriptRunner (شامل Script REST Endpoint و Behaviours) و Jira Automation. در این مقاله کل مسیر را قدم‌به‌قدم، همراه با کدها، شرح می‌دهم.

چه چیزی می‌خواستیم؟

سه نیاز مشخص داشتیم:

  1. انتخاب ریپوی واقعی در فرم ایشو: کاربر هنگام ساخت ایشو در جیرا، ریپوی مربوطه را از فهرست زنده‌ی پروژه‌های گیت‌لب انتخاب کند، نه اینکه نامش را دستی تایپ کند.
  2. ساخت خودکار ایشو در گیت‌لب: با ساخته شدن ایشو در جیرا، یک ایشوی متناظر در همان ریپوی انتخاب‌شده در گیت‌لب ساخته شود.
  3. همگام‌سازی برگشتی: وقتی در گیت‌لب اتفاقی روی ایشو می‌افتد (مثلاً بسته می‌شود)، ایشوی متناظر در جیرا هم به‌روز شود.

معماری کلی

نمای کلی اتصال Jira و GitLab
سه جریان داده‌ی مستقل: ۱) گرفتن فهرست ریپوها، ۲) ساخت ایشو در گیت‌لب، ۳) برگشت رویدادها به جیرا.

کل این اتصال روی یک قرارداد ساده سوار است:

عنوان ایشو در گیت‌لب همیشه با کلید ایشوی جیرا و یک اسلش شروع می‌شود. مثال: DEV-142/Fix login timeout on SSO callback

جیرا هنگام ساخت ایشو در گیت‌لب این عنوان را می‌سازد، و وقتی رویدادی از گیت‌لب برمی‌گردد، جیرا با جدا کردن بخش قبل از / می‌فهمد این رویداد مربوط به کدام ایشوی خودش است. به همین سادگی؛ نه دیتابیس واسط لازم است و نه جدول نگاشت.

بخش اول: فیلد انتخاب ریپو در جیرا

این بخش سه قدم دارد: یک Endpoint که فهرست ریپوها را از گیت‌لب می‌گیرد، یک فیلد سفارشی، و یک Behaviour که آن فیلد را به لیست چندانتخابی تبدیل می‌کند.

قدم ۱: ساخت Script REST Endpoint در ScriptRunner

از مسیر ScriptRunner ← REST Endpoints ← Create REST Endpoint یک Endpoint جدید ساختیم. این Endpoint با توکن گیت‌لب، پروژه‌های یک گروه (به‌همراه زیرگروه‌ها) را از API گیت‌لب می‌گیرد و خروجی را به قالبی تبدیل می‌کند که Behaviours می‌فهمد: آرایه‌ای از items که هر کدام یک value (شناسه‌ی پروژه) و یک label (نام پروژه) دارند.

نسخه‌ای که در پروژه پیاده‌سازی شد این بود (آدرس سرور و توکن عمداً حذف شده‌اند):

gitlabRepoQuery.groovy
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import groovyx.net.http.ContentType
import groovyx.net.http.RESTClient
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate

gitlabRepoQuery(httpMethod: "GET") { MultivaluedMap queryParams ->

    // GitLab API endpoint and token
    def gitlabApiEndpoint = "https://git.example.com/api/v4/groups/<GROUP_ID>/projects?include_subgroups=true&per_page=100"
    def bearerToken = "<GITLAB_TOKEN>"

    def rt = []
    def http = new RESTClient(gitlabApiEndpoint)
    http.headers['Authorization'] = "Bearer ${bearerToken}"

    def response = http.get(contentType: ContentType.JSON)

    if (response.status == 200) {
        def projects = response.data

        if (projects instanceof List) {
            rt = [
                items: projects.collect { project ->
                    [
                        value: project.id.toString(),   // GitLab project id
                        label: project.name.toString(), // shown to the user
                    ]
                }
            ]
        } else {
            log.warn("Invalid response format from GitLab API")
            return Response.serverError().build()
        }
    } else {
        log.warn("Failed to query GitLab API: ${response.status} - ${response.statusText}")
        return Response.serverError().build()
    }

    return Response.ok(new JsonBuilder(rt).toString()).build()
}

بعد از ذخیره، Endpoint در این آدرس در دسترس است:

text
<JIRA_BASE_URL>/rest/scriptrunner/latest/custom/gitlabRepoQuery

یک بار این آدرس را مستقیم در مرورگر (با کاربری که لاگین است) باز کنید؛ اگر JSONی با کلید items دیدید، این قدم درست کار می‌کند.

قدم ۲: ساخت فیلد سفارشی

یک فیلد سفارشی از نوع Text Field (single line) با نام GIT-REPO ساختیم و آن را به اسکرین‌های Create و Edit پروژه اضافه کردیم. فیلد عمداً متنی است؛ ظاهرِ لیستِ انتخابی را Behaviour به آن می‌دهد و مقدار انتخاب‌شده (شناسه‌ی پروژه‌ی گیت‌لب) به‌صورت متن در آن ذخیره می‌شود.

قدم ۳: تبدیل فیلد به لیست چندانتخابی با Behaviour

از مسیر ScriptRunner ← Behaviours یک Behaviour جدید ساختیم، آن را به پروژه و نوع ایشوهای موردنظر مپ کردیم و این کد را در بخش Initialiser گذاشتیم:

Behaviour · Initialiser
getFieldByName("GIT-REPO").convertToMultiSelect([
    ajaxOptions: [
        url           : getBaseUrl() + "/rest/scriptrunner/latest/custom/gitlabRepoQuery",
        minQueryLength: 2,
        keyInputPeriod: 500,
        formatResponse: "general",
    ]
])

معنای تنظیمات:

  • minQueryLength: 2: جست‌وجو بعد از تایپ حداقل دو حرف شروع می‌شود.
  • keyInputPeriod: 500: بعد از توقف تایپ، نیم ثانیه صبر می‌کند و بعد درخواست می‌فرستد؛ پس با هر کلید یک درخواست به گیت‌لب نمی‌رود.
  • formatResponse: "general": یعنی پاسخ Endpoint همان قالب استاندارد items / value / label است.
فیلد GIT-REPO بعد از اعمال Behaviour
نتیجه در فرم ایشو: کاربر نام ریپو را می‌بیند و انتخاب می‌کند؛ در پشت صحنه شناسه‌ی پروژه ذخیره می‌شود.

بخش دوم: ساخت ایشو در GitLab از روی جیرا

حالا که هر ایشو می‌داند به کدام ریپو تعلق دارد، ساخت ایشوی متناظر در گیت‌لب با یک قانون Jira Automation انجام می‌شود:

  • Trigger: Issue created (محدود به پروژه و نوع ایشوهای موردنظر)
  • Condition: فیلد GIT-REPO خالی نباشد
  • Action: Send web request

تنظیمات Action در پروژه به این شکل بود:

text
Method : POST
URL    : https://git.example.com/api/v4/projects/{{issue.GIT-REPO}}/issues?title={{issue.key.urlEncode}}/{{issue.summary.urlEncode}}&description={{issue.description.urlEncode}}
Header : PRIVATE-TOKEN: <GITLAB_TOKEN>

دو نکته در این آدرس مهم است:

  • {{issue.GIT-REPO}} همان شناسه‌ی پروژه‌ی گیت‌لب است که Behaviour در فیلد ذخیره کرده؛ پس ایشو دقیقاً در ریپوی انتخاب‌شده ساخته می‌شود.
  • عنوان به شکل {{issue.key}}/{{issue.summary}} ساخته می‌شود؛ همان قراردادی که بخش سوم به آن تکیه دارد. تابع urlEncode هم جلوی خراب شدن آدرس به‌خاطر فاصله و حروف فارسی را می‌گیرد.
اتوماسیون ساخت ایشو در GitLab
قانون Automation و ایشویی که در ریپوی انتخاب‌شده در گیت‌لب ساخته می‌شود.

بخش سوم: برگشت رویدادهای GitLab به جیرا

برای جهت برعکس، از ترکیب Webhook در گیت‌لب و تریگر Incoming webhook در Jira Automation استفاده کردیم:

  1. در جیرا یک قانون Automation با تریگر Incoming webhook ساختیم. جیرا برای این تریگر یک آدرس اختصاصی می‌سازد.
  2. در گیت‌لب، در Settings ← Webhooks پروژه، همین آدرس را وارد و رویدادهای موردنیاز (مثلاً Issues events) را انتخاب کردیم.
  3. در تریگر جیرا گزینه‌ی اجرای قانون روی «ایشوهای حاصل از یک جست‌وجوی JQL» را انتخاب کردیم و این JQL را نوشتیم:
text
issuekey = {{webhookData.object_attributes.title.substringBefore("/")}}

گیت‌لب در بدنه‌ی Webhook، اطلاعات ایشو را در object_attributes می‌فرستد. با substringBefore("/") بخش قبل از اسلش در عنوان، یعنی کلید جیرا، جدا می‌شود و JQL دقیقاً همان ایشو را پیدا می‌کند. از اینجا به بعد هر اقدامی که بخواهید روی آن ایشو قابل انجام است.

برگشت رویدادهای GitLab به جیرا
از رویداد در گیت‌لب تا اقدام روی ایشوی درست در جیرا.

برای نمونه، اقدامات زیر را می‌توان بر اساس نوع رویداد تعریف کرد:

text
If {{webhookData.object_attributes.action}} equals "close"
    → Transition issue to: Done
    → Comment: Closed in GitLab: {{webhookData.object_attributes.url}}

If {{webhookData.object_attributes.action}} equals "reopen"
    → Transition issue to: In Progress

نکته‌هایی که بهتر است از روز اول رعایت شود

نسخه‌ی بالا کار می‌کند، اما در محیط عملیاتی چند نکته تفاوت بزرگی ایجاد می‌کند:

۱. توکن را در کد ننویسید

توکنِ داخل اسکریپت برای هر کسی که به تنظیمات ScriptRunner دسترسی دارد قابل مشاهده است. چند پیشنهاد:

  • برای Endpoint از یک Group/Project Access Token با کمترین سطح دسترسی (read_api) استفاده کنید و برای ساخت ایشو یک توکن جدا با دسترسی api.
  • توکن را در یک متغیر محیطی روی سرور جیرا نگه دارید و در اسکریپت با System.getenv(...) بخوانید.
  • برای توکن تاریخ انقضا بگذارید و چرخش آن را در تقویم تیم ثبت کنید.

۲. فهرست بیشتر از ۱۰۰ ریپو را از دست ندهید

API گیت‌لب در هر صفحه حداکثر ۱۰۰ نتیجه برمی‌گرداند. اگر گروه بیشتر از ۱۰۰ پروژه داشته باشد، بقیه در فهرست دیده نمی‌شوند. راه‌حل، خواندن هدر X-Next-Page و گرفتن صفحه‌های بعدی است.

۳. متن جست‌وجو را به گیت‌لب بدهید

Behaviour متنی را که کاربر تایپ کرده با پارامتر query به Endpoint می‌فرستد. اگر Endpoint این پارامتر را نادیده بگیرد، کاربر هر چه تایپ کند همان فهرست ثابت را می‌بیند. کافی است query را به پارامتر search در API گیت‌لب بدهید.

۴. نام کامل ریپو را نمایش دهید

در گروه‌هایی با زیرگروه‌های متعدد، چند پروژه ممکن است نام یکسانی داشته باشند (مثلاً چند api). نمایش path_with_namespace به جای name (مثل backend/api و mobile/api) جلوی انتخاب اشتباه را می‌گیرد.

نسخه‌ی بهبودیافته‌ی Endpoint با هر چهار نکته:

gitlabRepoQuery.groovy · نسخه‌ی بهبودیافته
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import groovyx.net.http.ContentType
import groovyx.net.http.HttpResponseException
import groovyx.net.http.RESTClient
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate

// فقط کاربران این گروه به Endpoint دسترسی دارند
gitlabRepoQuery(httpMethod: "GET", groups: ["jira-software-users"]) { MultivaluedMap queryParams ->

    def gitlabUrl = "https://git.example.com"
    def groupId   = "<GROUP_ID>"
    def token     = System.getenv("GITLAB_READ_TOKEN")   // توکن در کد نمی‌ماند
    def query     = queryParams.getFirst("query")?.toString()?.trim() ?: ""

    def http = new RESTClient(gitlabUrl)
    http.headers["PRIVATE-TOKEN"] = token

    def projects = []
    def page = "1"
    try {
        while (page) {
            def resp = http.get(
                path: "/api/v4/groups/${groupId}/projects".toString(),
                query: [include_subgroups: true, archived: false, per_page: 100,
                        page: page, search: query, order_by: "name", sort: "asc"],
                contentType: ContentType.JSON
            )
            projects.addAll(resp.data as List)
            page = resp.headers["X-Next-Page"]?.value ?: null   // آخرین صفحه: خالی
        }
    } catch (HttpResponseException e) {
        log.warn("GitLab API error: ${e.statusCode} - ${e.message}")
        return Response.serverError().build()
    }

    def items = projects.collect { p ->
        [value: p.id.toString(), label: p.path_with_namespace.toString()]
    }
    return Response.ok(new JsonBuilder([items: items, total: items.size()]).toString()).build()
}

۵. برای توضیحات طولانی، بدنه‌ی JSON بفرستید

فرستادن description در آدرس (Query String) برای توضیحات کوتاه کار می‌کند، اما توضیحات طولانی ممکن است از محدودیت طول آدرس در وب‌سرور یا پراکسی عبور کند. امن‌تر این است که در Send web request بدنه را به‌صورت JSON بفرستید:

Send web request · Body
{
  "title": "{{issue.key}}/{{issue.summary.jsonEncode}}",
  "description": "{{issue.description.jsonEncode}}\n\nJira: {{issue.url}}",
  "labels": "jira"
}

هدر Content-Type: application/json را هم اضافه کنید. اگر گزینه‌ی انتظار برای پاسخ را فعال کنید، با {{webResponse.body.web_url}} می‌توانید لینک ایشوی گیت‌لب را به‌صورت کامنت یا در یک فیلد، روی ایشوی جیرا ثبت کنید.

۶. حواستان به انتخاب چند ریپو باشد

فیلد چندانتخابی است، اما آدرس ساخت ایشو فقط یک شناسه‌ی پروژه می‌پذیرد. اگر کاربر دو ریپو انتخاب کند، {{issue.GIT-REPO}} شامل چند مقدار می‌شود و درخواست خطا می‌دهد. دو راه دارید: اگر هر ایشو فقط به یک ریپو مربوط است، به جای convertToMultiSelect از convertToSingleSelect استفاده کنید؛ وگرنه در Automation فقط اولین مقدار را برای ساخت ایشو بفرستید.

۷. جلوی حلقه و نویز را بگیرید

وقتی جیرا در گیت‌لب ایشو می‌سازد، گیت‌لب برای همان ایشو یک رویداد open به جیرا برمی‌گرداند. اگر قانون برگشتی فقط روی رویدادهای مشخص (close، reopen و…) کار کند، نه حلقه‌ای ساخته می‌شود و نه کامنت‌های بی‌مصرف.

۸. از قرارداد عنوان محافظت کنید

کل همگام‌سازی برگشتی به KEY/ در ابتدای عنوان وابسته است. اگر کسی عنوان را در گیت‌لب عوض کند و کلید را پاک کند، اتصال آن ایشو قطع می‌شود. این قرارداد را به تیم توسعه آموزش دهید. همین الگو را می‌توانید برای عنوان Merge Requestها هم به کار ببرید و با رویدادهای Merge request events، وضعیت ایشو را با چرخه‌ی کد هماهنگ کنید.

۹. اتصال شبکه و گواهی SSL را از قبل بررسی کنید

جیرا باید به آدرس گیت‌لب دسترسی شبکه داشته باشد و برعکس، گیت‌لب باید بتواند آدرس Incoming webhook جیرا را صدا بزند. اگر گیت‌لب سازمانی با گواهی داخلی سازمان کار می‌کند، آن گواهی باید در Truststore جاوای سرور جیرا معتبر باشد؛ در غیر این صورت درخواست‌های Endpoint با خطای SSL شکست می‌خورند. آدرس Incoming webhook را هم مثل یک رمز نگه دارید، چون هر کسی آن را داشته باشد می‌تواند قانون را اجرا کند.

چک‌لیست پیاده‌سازی

  • ساخت توکن در گیت‌لب با کمترین دسترسی لازم و نگهداری امن آن
  • ساخت Script REST Endpoint و تست خروجی آن در مرورگر
  • ساخت فیلد متنی GIT-REPO و افزودن به اسکرین‌های Create و Edit
  • ساخت Behaviour، مپ کردن به پروژه و گذاشتن کد در Initialiser
  • قانون Automation برای ساخت ایشو در گیت‌لب با عنوان KEY/Summary
  • قانون Automation با تریگر Incoming webhook و JQL مبتنی بر substringBefore("/")
  • تعریف Webhook در گیت‌لب روی رویدادهای موردنیاز
  • تست کامل: ساخت ایشو در جیرا ← دیدن آن در گیت‌لب ← بستن در گیت‌لب ← تغییر وضعیت در جیرا

جمع‌بندی

با سه قطعه‌ی ساده (یک Endpoint، یک Behaviour و دو قانون Automation) جیرا و گیت‌لب سازمانی به هم وصل شدند: کاربر ریپوی واقعی را از فرم ایشو انتخاب می‌کند، ایشو خودکار در گیت‌لب ساخته می‌شود، و تغییرات گیت‌لب به جیرا برمی‌گردد. بدون پلاگین تجاری و بدون سرویس واسط. قلب این راهکار هم یک قرارداد نام‌گذاری ساده است: KEY/ در ابتدای عنوان.

اگر شما هم می‌خواهید جیرا را به گیت‌لب یا ابزارهای دیگر سازمان‌تان وصل کنید، در اطلسیا می‌توانیم در طراحی و پیاده‌سازی این اتصال کنارتان باشیم.

پروژه‌ای مشابه در سازمان خودتان دارید؟ خوشحال می‌شویم درباره‌ی آن صحبت کنیم.

تماس با ما
مرجع تخصصی مشاوره ابزارهای اتلسیان، جیرا و راهکارهای چابک‌سازی سازمانی؛ همراه سازمان‌ها از طراحی فرآیند تا استقرار و پشتیبانی.
مسعود سجادمسعود سجادپروفایل لینکدین
سایت
خدمات
کلیه حقوق محفوظ است © 2026 اطلسیا
Jira، Confluence و Atlassian علامت‌های تجاری شرکت Atlassian هستند.