لماذا إدارة الإصدارات يدويًا هي كابوس لكل مطور Go
\nهل سبق لك أن أمسكت بنفسك تفكر: "أي إصدار يعمل حاليًا على الإنتاج؟" أو أضعت ساعة لتفهم أي commit تسبب في خطأ في ملف ثنائي تم بناؤه قبل شهر؟ إذا كان الأمر كذلك - فقد وصلت إلى المكان الصحيح. سؤال "Application auto build versioning" هو واحد من أكثر الأسئلة شيوعًا على Stack Overflow (آلاف المشاهدات وعشرات الحلول). وليس من دون سبب: في التطوير الحديث، حيث تقوم خطوط CI/CD بتشغيل البناءات كل 10 دقائق، فإن وضع الإصدارات يدويًا هو طريق نحو الفوضى.
\nفي هذه المقالة، سنشرح الإصدار التلقائي للبناءات في Go من الألف إلى الياء. ستتعلم كيفية تنفيذ نظام يقوم تلقائيًا بتعيين رقم الإصدار، وhash الـ commit، وتاريخ البناء، وحتى علامة المستودع "المتسخ". سيعمل الكود في كل من التطوير المحلي وفي GitHub Actions أو GitLab CI أو Jenkins.
\n\nالمشكلة: ما الخطأ في ملف version.go اليدوي؟
\nكثير من المطورين المبتدئين ينشئون ملف version.go ويكتبون:
package main\n\nvar Version = "1.0.0"\nوهذا يعمل تمامًا حتى أول commit. بعد ذلك يبدأ:
\n- \n
- "نسيت تحديث الإصدار قبل البناء" \n
- "على الإنتاج إصدار، وفي الكود إصدار آخر" \n
- "أي commit يتوافق مع هذا البناء؟" \n
المشكلة هي أن الإصدار هو بيانات وصفية للبناء، وليس جزءًا من الكود المصدري. يجب أن يتم توليده تلقائيًا في مرحلة التجميع.
\n\nالحل: ldflags - السلاح السري لـ Go
\nمترجم Go يسمح "بإدراج" قيم المتغيرات أثناء البناء من خلال العلم -ldflags. هذا هو auto build versioning في شكله الكلاسيكي. نحن لا نخزن الإصدار في الكود - بل نمرره إلى المترجم.
الخطوة 1: تحديد هيكل الإصدار
\nلننشئ حزمة version بمتغيرات سيتم ملؤها في مرحلة البناء:
// version/version.go\npackage version\n\nvar (\n // Version — الإصدار الدلالي (يُضبط يدويًا أو عبر git tag)\n Version = "dev"\n\n // Commit — hash قصير للـ commit\n Commit = "none"\n\n // BuildTime — وقت البناء بتنسيق ISO 8601\n BuildTime = "unknown"\n\n // IsDirty — علامة أن المستودع يحتوي على تغييرات غير ملتزمة\n IsDirty = "false"\n)\n\nfunc String() string {\n return "v" + Version + " (commit: " + Commit + ", built: " + BuildTime + ")"\n}\n\nالخطوة 2: البناء باستخدام ldflags
\nالآن الجزء الممتع. بدلاً من go build، سنستخدم سكريبت أو Makefile يقوم بحساب القيم المطلوبة وتمريرها إلى المترجم:
#!/bin/bash\n# build.sh\n\nVERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")\nCOMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "none")\nBUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")\nIS_DIRTY=$(git diff --quiet || echo "true")\n\ngo build -ldflags="\\\n -X 'main/version.Version=$VERSION' \\\n -X 'main/version.Commit=$COMMIT' \\\n -X 'main/version.BuildTime=$BUILD_TIME' \\\n -X 'main/version.IsDirty=$IS_DIRTY'" \\\n -o myapp .\nهام: المسار main/version.Version يعتمد على هيكل مشروعك. إذا كانت الحزمة version موجودة في الجذر - اكتب main/version.Version. إذا كانت في دليل منفصل - github.com/your-username/project/version.Version.
الخطوة 3: الاستخدام في التطبيق
\nالآن في أي مكان في البرنامج يمكنك عرض الإصدار:
\n// main.go\npackage main\n\nimport (\n "fmt"\n "main/version"\n)\n\nfunc main() {\n fmt.Println("App version:", version.String())\n fmt.Println("Is dirty repo:", version.IsDirty)\n}\nقم بالبناء والتشغيل:
\n$ ./build.sh\n$ ./myapp\nApp version: v1.2.3 (commit: a1b2c3d, built: 2024-03-15T10:30:00Z)\nIs dirty repo: false\n\n