در پست قبلی نشان دادم که چگونه میتوان تمام اسکریپتهای مورد استفاده در CI را در یک مخزن واحد نگه داشت. حالا بیایید ببینیم چه اسکریپتهای پیشرفتهتری میتوانید در آنجا قرار دهید.
این بار میخواهم نشان دهم که چگونه نسخهبندی خودکار را به خط لوله خود اضافه کنید. همچنین خواهید دید که چگونه میتوانید از طریق وظایف CI، تغییرات را به مخزن خود ارسال (push) کنید. اما ابتدا، بیایید با مقدمهای شروع کنیم.
انتخاب جریان کاری
یکی از چیزهایی که در مورد GitLab دوست دارم، انعطافپذیری آن برای تنظیم جریان کاری CI است. چه یک بار در هفته منتشر کنید، چه هر دو ماه یک بار، چه از gitflow کلاسیک پیروی کنید و چه از continuous deployment استفاده کنید، تنها مسئله تنظیمات کانفیگ است.
به عنوان یک طرفدار اصول continuous delivery، معمولا به یک شاخه master
محافظتشده و شاخههای feature کوتاهمدت پایبند هستم که پس از بازبینی کد ادغام میشوند. هر تغییر در شاخه master
بهطور خودکار در نوعی محیط staging (ترجیحا مشابه محیط production) مستقر میشود و سپس میتواند بهطور دستی به production منتقل شود. اگر مجموعهای قوی از تستهای چند لایه داشته باشید، این جریان کاری به شما اجازه میدهد چندین بار در روز استقرار انجام دهید.
نسخهبندی
هر جریانی که انتخاب کنید، نسخهبندی برنامه شما در مراحل مختلف خط لوله ضروری میشود. به عنوان مثال، دانستن اینکه در صورت وقوع مشکل دقیقا باید به کدام نسخه برگردید، بسیار حیاتی است. شما همیشه باید بتوانید بهسرعت commit مربوط به یک شماره نسخه خاص را پیدا کنید.
سیستم نسخهبندی به خود شما بستگی دارد و بر اساس جریان کاری شما متفاوت خواهد بود. من معمولا از Semantic Versioning استفاده میکنم، زیرا طبیعی به نظر میرسد و در حال حاضر یک استاندارد برای بسیاری از پروژهها، بهویژه کتابخانهها، است. بنابراین اولین commit در شاخه master
شماره نسخه 1.0.0
را میگیرد، نسخه بعدی 1.0.1
خواهد بود، سپس 1.0.2
و به همین ترتیب (میتوانید از 0.0.1
شروع کنید و 1.0.0
را بهعنوان اولین انتشار عمومی در نظر بگیرید).
نسخهها را کجا نگه داریم؟ چندین نکته مفید از تجربه من:
- نسخه را در کد یا یک فایل commitشده در مخزن نگه ندارید.
- Git tags کاملا مناسب هستند تا نسخهها را به commitها پیوند دهند.
- فرآیند را خودکار کنید. وقت توسعهدهندگان را برای انتخاب شماره نسخه تلف نکنید.
بنابراین، بهطور خلاصه، شما نیاز دارید که فرآیند برچسبگذاری نسخه خودکار را به سیستم CI خود متصل کنید.
پیادهسازی
بخش اصلی مدیریت نسخهبندی، یک اسکریپت است که در هر وظیفه (job) روی شاخه master
اجرا میشود. این اسکریپت باید آخرین نسخه را بگیرد، آن را افزایش دهد، یک تگ اضافه کند و آن را به مخزن remote ارسال کند.
توجه: احتمالا نیازی به تولید نسخه در شاخههای دیگر ندارید. البته موارد استفاده معتبری برای این وجود دارد، اما این موضوعی است برای یک پست دیگر.
این بار میخواهم از Python و کتابخانه python-semver استفاده کنم. اگر ترجیح میدهید با bash کار کنید، نگاهی به ابزار semver-tool بیندازید.
بیایید ببینیم اسکریپت چه کاری باید انجام دهد.
۱. آخرین نسخه را بگیرید و آن را افزایش دهید.
این کار به اندازه کافی ساده است. میتوانید با اجرای دستور git describe --tags
آخرین تگ را استخراج کنید. سپس از کتابخانه semver برای افزایش نسخه استفاده کنید.
تابع main
ما ممکن است چیزی شبیه به این باشد (کد کامل):
def main():
try:
latest = git("describe", "--tags").decode().strip()
except subprocess.CalledProcessError:
# No tags in the repository
version = "1.0.0"
else:
# Skip already tagged commits
if '-' not in latest:
print(latest)
return 0
version = bump(latest)
tag_repo(version)
print(version)
return 0
البته یک نکته وجود دارد: چگونه تصمیم میگیرید که نسخه پچ، نسخه ماینور یا نسخه اصلی را بالا ببرید؟
def bump(latest):
# TODO decide what to bump
# Then use bump_patch, bump_minor or bump_major
return semver.bump_patch(latest)
مطمئن شوید که این تغییر را بهنوعی در کامیت علامتگذاری کنید. من این موضوع را در مثال پیادهسازی نخواهم کرد تا ساده بماند، اما در اینجا چند ایدهای که میتواند کارساز باشد آوردهام:
- پیشفرض بودن افزایش نسخهی پَچ، چون این رایجترین عملیات است.
- قصد برای افزایش نسخهی ماینور یا ماژور میتواند در پیام کامیت علامتگذاری شود؛ بهعنوان مثال، با استفاده از عبارتی مانند
#minor
یاbump-minor
. - بهطور مشابه، میتوانید به درخواستهای مرج (Merge Requests) برچسبهایی مانند
bump-minor
وbump-major
اضافه کنید. - میتوانید یک اسکریپت طراحی کنید که تشخیص دهد آیا تغییرات شکستن سازگاری (breaking changes) یا قابلیتهای جدید معرفی شدهاند یا خیر.
۲. افزودن یک تگ جدید و پوشکردن آن به مخزن ریموت
احراز هویت
این مرحله نیاز به دسترسی نوشتن CI job به مخزن دارد. متأسفانه، در حال حاضر GitLab از پوشکردن تغییرات به مخزن بهصورت پیشفرض پشتیبانی نمیکند. توکنهای دیپلوی که در پست قبلی توضیح داده شد، در اینجا کارایی ندارند، زیرا آنها فقط دسترسی خواندن دارند. بنابراین باید از کلیدهای دیپلوی استفاده کنیم.
ابتدا، یک کلید جدید در دستگاه محلی خود ایجاد کنید (بدون رمزعبور):
ssh-keygen -t rsa -b 4096
بخش عمومی کلید را بهعنوان یک Deploy Key جدید در بخش Settings -> Repository اضافه کنید. مطمئن شوید که گزینهی “Write access allowed” فعال باشد.
بخش خصوصی کلید را بهعنوان یک متغیر جدید در بخش CI/CD اضافه کنید. نام آن بستگی به انتخاب شما دارد؛ من از SSH_PRIVATE_KEY
استفاده میکنم.
پس از ذخیره کلیدها در GitLab، بهتر است فایل کلید خصوصی را حذف کنید (یا حتی بهتر، آن را بهطور کامل پاک [shred] کنید).
تنها کاری که باقی مانده، اضافهکردن کلید SSH به تعریف CI است. چندین روش برای انجام این کار وجود دارد، یکی از آنها به شکل زیر است (اگر از GitLab خودمیزبان استفاده میکنید، gitlab.com
را با نام میزبان خود جایگزین کنید):
script:
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan gitlab.com >> ~/.ssh/known_hosts && chmod 644 ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- ssh-add <(echo "$SSH_PRIVATE_KEY")
پوش کردن تگ
از آنجا که مخزن با استفاده از HTTPS کون شده (و نه SSH)، بنابراین git push
کافی نیست. سادهترین راهحل تغییر URL ریموت از طریق یک عبارت باقاعده است.
def tag_repo(tag):
url = os.environ["CI_REPOSITORY_URL"]
# Transforms the repository URL to the SSH URL
# Example input: https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@gitlab.com/threedotslabs/ci-examples.git
# Example output: git@gitlab.com:threedotslabs/ci-examples.git
push_url = re.sub(r'.+@([^/]+)/', r'git@\1:', url)
git("remote", "set-url", "--push", "origin", push_url)
git("tag", tag)
git("push", "origin", tag)
3. دادههای مربوط به نسخه را به مراحل بعد پاس دهید
به احتمال زیاد تعداد جابهای پایپلاین که نیاز به دادههای نسخه دارند، بیشتر از یکی است. به این منظور کافی است که فایلی محتوی نسخه ایجاد کرده و آن را پاس دهیم.
version:
image: python:3.7-stretch
stage: version
script:
- pip install semver
- $SCRIPTS_DIR/common/gen-semver > version
artifacts:
paths:
- version
only:
- branches
build:
image: golang:1.11
stage: build
script:
- export VERSION="unknown"
- "[ -f ./version ] && export VERSION=$(cat ./version)"
- $SCRIPTS_DIR/golang/build-semver . example-server main.Version "$VERSION"
artifacts:
paths:
- bin/
only:
- branches
اگر پست اسکریپتهای مشترک را از دست دادهاید یا نمیخواهید از آن استفاده کنید، میتوانید اسکریپت را در مخزن (repository) برنامه خود نیز ثبت (commit) کنید.
مراحل بعدی اکنون میتوانند فایل ./version
را بخوانند. همچنین میتوانید آن را با یک خط کد که در بخش before_script
قرار میگیرد، راحتتر کنید:
before_script:
- [ -f ./version ] && export VERSION=$(cat ./version)
جلوگیری از اجرا روی تگها
به یاد داشته باشید که در تعریف مرحلهی خود، تنظیم مناسب only
را قرار دهید، زیرا در غیر این صورت، پوشکردن تگهای خودکار باعث اجرای پایپلاینهای جدید میشود. محدود کردن اجرا به برنچها کار سادهای است:
only:
- branches
دربارهی Changelogs چطور؟
برخی از گردشکارها (Workflows) نسخه را بر اساس فایل CHANGELOG در مخزن تولید میکنند. من این رویکرد را پیشنهاد نمیکنم، چون این کار توسعهدهندگان را مجبور به تعیین نسخهها میکند، پیامهای کامیت را تکرار میکند و مستعد بروز تضادهای مرج (Merge Conflicts) است.
در عوض، میتوانید گیت لاگ (git log) را بهعنوان یک changelog در نظر بگیرید (روی نوشتن پیامهای کامیت عالی تمرکز کنید). با راهاندازی نسخهبندی خودکار، همه چیز مورد نیاز در کامیت موجود است: نویسنده، تاریخ، نسخه و پیام. CI میتواند بهطور خودکار یک فایل changelog برای شما تولید کرده و با هر نسخهی جدید آن را در جایی آپلود کند.
خلاصه
این تنها یک راهاندازی اولیه است که میتوان برای موارد خاصتر آن را تنظیم کرد. اگر سؤالی دارید یا میخواهید فرآیند نسخهبندی خود را به اشتراک بگذارید، میتوانید در توییتر با من تماس بگیرید.
نمونههای کامل را اینجا ببینید:
لینکهای خارجی:
- نسخهبندی معنایی
- نگاهی به ابزار semantic-release بیندازید تا برخی ایدهها را مقایسه کنید.