تیم توسعهی یک سازمان نرمافزاری با GitLab سازمانی
چالش
کار تیم توسعه بین Jira Software و GitLab سازمانی تقسیم شده بود: یک کار دو بار ثبت میشد، وضعیتها در دو طرف با هم نمیخواند و معلوم نبود هر تسک به کدام ریپو مربوط است.
راهکار
بدون خرید پلاگین، با ScriptRunner (Script REST Endpoint و Behaviours) و دو قانون Jira Automation؛ و یک قرارداد ساده: عنوان ایشو در گیتلب همیشه با کلید جیرا و / شروع میشود.
نتیجه
کاربر ریپوی واقعی را از فرم ایشو انتخاب میکند، ایشو خودکار در همان ریپوی گیتلب ساخته میشود و تغییرات گیتلب (مثل بسته شدن ایشو) به جیرا برمیگردد.
در خیلی از سازمانها کار تیم توسعه در دو دنیای جدا اتفاق میافتد: برنامهریزی، اولویتبندی و گزارشگیری در Jira Software، و کد، ایشوهای فنی و Merge Requestها در GitLab سازمانی که روی سرورهای خود شرکت نصب شده است. نتیجه معمولاً آشناست: یک کار دو بار ثبت میشود، وضعیتِ «تمامشده» در یک طرف هست و در طرف دیگر نیست، و وقتی کسی میپرسد «این تسک مربوط به کدام ریپو است؟» باید از توسعهدهنده سؤال کرد.
در این پروژه هدف این بود که این دو دنیا بدون خرید پلاگین اضافه به هم وصل شوند؛ فقط با ابزارهایی که از قبل روی جیرا داشتیم: ScriptRunner (شامل Script REST Endpoint و Behaviours) و Jira Automation. در این مقاله کل مسیر را قدمبهقدم، همراه با کدها، شرح میدهم.
چه چیزی میخواستیم؟
سه نیاز مشخص داشتیم:
انتخاب ریپوی واقعی در فرم ایشو: کاربر هنگام ساخت ایشو در جیرا، ریپوی مربوطه را از فهرست زندهی پروژههای گیتلب انتخاب کند، نه اینکه نامش را دستی تایپ کند.
ساخت خودکار ایشو در گیتلب: با ساخته شدن ایشو در جیرا، یک ایشوی متناظر در همان ریپوی انتخابشده در گیتلب ساخته شود.
همگامسازی برگشتی: وقتی در گیتلب اتفاقی روی ایشو میافتد (مثلاً بسته میشود)، ایشوی متناظر در جیرا هم بهروز شود.
معماری کلی
سه جریان دادهی مستقل: ۱) گرفتن فهرست ریپوها، ۲) ساخت ایشو در گیتلب، ۳) برگشت رویدادها به جیرا.
کل این اتصال روی یک قرارداد ساده سوار است:
عنوان ایشو در گیتلب همیشه با کلید ایشوی جیرا و یک اسلش شروع میشود.
مثال: 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 (نام پروژه) دارند.
نسخهای که در پروژه پیادهسازی شد این بود (آدرس سرور و توکن عمداً حذف شدهاند):
یک بار این آدرس را مستقیم در مرورگر (با کاربری که لاگین است) باز کنید؛ اگر JSONی با کلید items دیدید، این قدم درست کار میکند.
قدم ۲: ساخت فیلد سفارشی
یک فیلد سفارشی از نوع Text Field (single line) با نام GIT-REPO ساختیم و آن را به اسکرینهای Create و Edit پروژه اضافه کردیم. فیلد عمداً متنی است؛ ظاهرِ لیستِ انتخابی را Behaviour به آن میدهد و مقدار انتخابشده (شناسهی پروژهی گیتلب) بهصورت متن در آن ذخیره میشود.
قدم ۳: تبدیل فیلد به لیست چندانتخابی با Behaviour
از مسیر ScriptRunner ← Behaviours یک Behaviour جدید ساختیم، آن را به پروژه و نوع ایشوهای موردنظر مپ کردیم و این کد را در بخش Initialiser گذاشتیم:
minQueryLength: 2: جستوجو بعد از تایپ حداقل دو حرف شروع میشود.
keyInputPeriod: 500: بعد از توقف تایپ، نیم ثانیه صبر میکند و بعد درخواست میفرستد؛ پس با هر کلید یک درخواست به گیتلب نمیرود.
formatResponse: "general": یعنی پاسخ Endpoint همان قالب استاندارد items / value / label است.
نتیجه در فرم ایشو: کاربر نام ریپو را میبیند و انتخاب میکند؛ در پشت صحنه شناسهی پروژه ذخیره میشود.
بخش دوم: ساخت ایشو در 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 هم جلوی خراب شدن آدرس بهخاطر فاصله و حروف فارسی را میگیرد.
قانون Automation و ایشویی که در ریپوی انتخابشده در گیتلب ساخته میشود.
بخش سوم: برگشت رویدادهای GitLab به جیرا
برای جهت برعکس، از ترکیب Webhook در گیتلب و تریگر Incoming webhook در Jira Automation استفاده کردیم:
در جیرا یک قانون Automation با تریگر Incoming webhook ساختیم. جیرا برای این تریگر یک آدرس اختصاصی میسازد.
در گیتلب، در Settings ← Webhooks پروژه، همین آدرس را وارد و رویدادهای موردنیاز (مثلاً Issues events) را انتخاب کردیم.
در تریگر جیرا گزینهی اجرای قانون روی «ایشوهای حاصل از یک جستوجوی JQL» را انتخاب کردیم و این JQL را نوشتیم:
گیتلب در بدنهی Webhook، اطلاعات ایشو را در object_attributes میفرستد. با substringBefore("/") بخش قبل از اسلش در عنوان، یعنی کلید جیرا، جدا میشود و JQL دقیقاً همان ایشو را پیدا میکند. از اینجا به بعد هر اقدامی که بخواهید روی آن ایشو قابل انجام است.
از رویداد در گیتلب تا اقدام روی ایشوی درست در جیرا.
برای نمونه، اقدامات زیر را میتوان بر اساس نوع رویداد تعریف کرد:
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) جلوی انتخاب اشتباه را میگیرد.
فرستادن description در آدرس (Query String) برای توضیحات کوتاه کار میکند، اما توضیحات طولانی ممکن است از محدودیت طول آدرس در وبسرور یا پراکسی عبور کند. امنتر این است که در Send web request بدنه را بهصورت JSON بفرستید:
هدر 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/ در ابتدای عنوان.
اگر شما هم میخواهید جیرا را به گیتلب یا ابزارهای دیگر سازمانتان وصل کنید، در اطلسیا میتوانیم در طراحی و پیادهسازی این اتصال کنارتان باشیم.
پروژهای مشابه در سازمان خودتان دارید؟ خوشحال میشویم دربارهی آن صحبت کنیم.