أساسيات التكامل المستمر

المخرجات والحزم المبنية

18 دقيقة الدرس 6 من 28

المخرجات والحزم المبنية

كل خط أنابيب CI ينتج مخرجات: ملفات ثنائية مُجمَّعة، صور حاويات، تقارير اختبارات، ملفات تغطية، حزم موقَّعة. تُسمَّى هذه المخرجات Artifacts. التعامل مع هذه المخرجات باعتبارها عناصر أساسية — بإصدارات حتمية، وتخزين آمن، وتمرير موثوق بين المراحل — هو ما يُفرِّق خط الأنابيب الهاوي عن النظام الإنتاجي على مستوى كبرى شركات التقنية.

ما الذي يُعدّ Artifact؟

المخرج هو أي ملف تنتجه مرحلة من مراحل خط الأنابيب إما ليُستهلك في مرحلة لاحقة، أو لينشر للاستخدام الخارجي. تشمل الأمثلة الشائعة:

  • الملفات الثنائية المُجمَّعة — Go، Rust، JARs/WARs لـ Java، ملفات DLL لـ .NET
  • صور الحاويات — طبقات بصيغة OCI مرفوعة إلى سجل الصور
  • حزم اللغات — tarballs لـ npm، wheels لـ Python، gems لـ Ruby، artifacts لـ Maven
  • المواقع الثابتة — ناتج next build أو hugo أو vite build
  • تقارير الاختبارات والتغطية — JUnit XML، lcov HTML، ملفات SARIF
  • خطط البنية التحتية — JSON لـ terraform plan المحفوظ قبل التطبيق
  • أرشيفات الإصدار — tarballs أو ZIPs مرفقة بإصدار GitHub Release

تخزين المخرجات: أين وَلماذا يهم

يجب تخزين المخرجات خارج بيئة التنفيذ المؤقتة (Runner). حين يُعاد استخدام آلة Runner — أو حين تحتاج مهمة موازية على آلة مختلفة إلى المخرج — يجب استرجاعه من موقع ثابت ومُصادق عليه. المستويات الرئيسية هي:

  • مخازن CI الأصيلة — GitHub Actions Artifacts (مدعومة بـ Azure Blob)، GitLab Job Artifacts (متوافقة مع S3). لا تتطلب إعدادًا، لكنها محدودة بالحجم (GitHub: 500 ميغابايت للمخرج الواحد، 10 غيغابايت للمستودع افتراضيًا) وبفترة احتفاظ قصيرة (90 يومًا).
  • سجلات الحزم — GitHub Packages، GitLab Package Registry، JFrog Artifactory، Sonatype Nexus. المكان المناسب للمخرجات القابلة للنشر ذات الإصدارات (JAR، npm، صورة Docker). تفرض اللاتغييرية باتفاق (لا يمكن استبدال الإصدار المنشور 1.2.3).
  • التخزين الكائني — AWS S3، GCS، Azure Blob. تستخدمه الفرق الكبيرة لكل ما لا يناسب سجل الحزم: خطط Terraform، نقاط تفتيش نماذج ML الضخمة، مقاطع فيديو اختبارات المتصفح.
اللاتغييرية هي العقد. بمجرد نشر مخرج تحت وسم إصدار (مثل v2.4.1)، يجب ألا يُستبدل أبدًا. أي تغيير — حتى بايت واحد — يستلزم إصدارًا جديدًا. لهذا السبب ترفض سجلات الحزم إعادة رفع نفس الإصدار افتراضيًا: المخرج القابل للتغيير يُفسد سلسلة التوريد بأكملها.

مخططات الإصدار الحتمية

يجب أن تكون إصدارات المخرجات فريدة، وقابلة للتتبع إلى commit محدد، وقابلة للترتيب. الأنماط الثلاثة المستخدمة في الإنتاج:

  1. SemVer من وسم gitv2.4.1 يُطلَق بـ git tag v2.4.1. معيار للحزم العامة. الأدوات: git describe --tags، semantic-release.
  2. لاحقة SHA للcommit2.4.1-abc1234. كل merge إلى main ينتج مخرجًا. الـ SHA يجعل الإصدار قابلًا للتتبع دون وسم. يُستخدم كثيرًا للخدمات الداخلية.
  3. CalVer + رقم البناء2025.06.1042. شائع في الـ Monorepos وإصدارات التطبيقات المحمولة (App Store يتطلب أرقامًا عددية). رقم البناء هو معرف تشغيل CI.

في GitHub Actions تتوفر SHA الكاملة عبر ${{ github.sha }} (40 حرفًا) — اقطعها في bash: SHA=$(echo $GITHUB_SHA | head -c8).

Artifact flow through pipeline stages Build compile + package Test unit + integration Publish push to registry Deploy pull & rollout Artifact Store S3 / Registry / CI cache upload download pull image myapp:2.4.1-abc1234f version = semver + short SHA — immutable once stored
تُرفع المخرجات إلى مخزن مركزي بعد مرحلة البناء، ثم تُنزَّل من قِبَل المراحل اللاحقة — لا يوجد نقل مباشر للملفات بين بيئات التنفيذ.

تمرير المخرجات بين المراحل (GitHub Actions)

بيئات التنفيذ (Runners) آلات افتراضية معزولة. الملفات المكتوبة على القرص من قِبَل مهمة واحدة تختفي حين تنتهي تلك المهمة. النمط المعتمد: الرفع في نهاية مهمة البناء، والتنزيل في بداية كل مهمة تحتاج المخرج. توفر GitHub Actions الإجراءين actions/upload-artifact و actions/download-artifact لهذا الغرض.

# .github/workflows/ci.yml — تمرير المخرجات بين المراحل name: CI on: push: branches: [main] jobs: build: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.sha }} steps: - uses: actions/checkout@v4 - name: Compute version id: version run: echo "sha=$(echo $GITHUB_SHA | head -c8)" >> $GITHUB_OUTPUT - name: Build binary run: | go build -ldflags="-X main.Version=${{ steps.version.outputs.sha }}" \ -o dist/myapp ./cmd/myapp - name: Upload artifact uses: actions/upload-artifact@v4 with: name: myapp-${{ steps.version.outputs.sha }} path: dist/ retention-days: 7 integration-test: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download artifact uses: actions/download-artifact@v4 with: name: myapp-${{ needs.build.outputs.version }} path: dist/ - name: Run integration tests run: | chmod +x dist/myapp ./dist/myapp --self-test
حدِّد retention-days دائمًا. القيمة الافتراضية 90 يومًا. على نطاق Google وMeta، يُنهك التخزين المتاح خلال أسابيع لأن كل Pull Request ينتج مخرجات. اضبط مدة احتفاظ قصيرة (3-7 أيام) للمخرجات الوسيطة، وأطول (90-365 يومًا) فقط لمخرجات الإصدارات التي قد تحتاج تحقيقًا بعد أشهر.

النشر إلى سجل الحزم

صور الحاويات هي المخرج الأكثر شيوعًا في الإنتاج. تُصادق خطوة النشر على السجل، وتضع وسمًا على الصورة بوسم خاص بالـ commit ووسم latest للراحة، ثم ترفع كليهما. يُسرِّع استخدام Docker BuildKit وتخزين الطبقات مؤقتًا هذه العملية كثيرًا في البناءات المتكررة.

publish: needs: integration-test runs-on: ubuntu-latest permissions: packages: write contents: read env: REGISTRY: ghcr.io IMAGE: ghcr.io/${{ github.repository }} steps: - uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE }} tags: | type=sha,prefix=,format=short type=ref,event=branch type=semver,pattern={{version}} - name: Build and push image uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max

قابلية تكرار البناء (Reproducibility)

البناء قابل للتكرار حين ينتج نفس commit المصدر دائمًا مخرجات متطابقة بايتيًا. هذا مهم للأمان (يمكن التحقق من أن الثنائي يطابق مصدره) وللتشخيص (يمكن إعادة بناء إصدار قبل ستة أشهر للتحقيق في ثغرة CVE). تحقيق ذلك يتطلب:

  • حفظ ملفات القفل في المستودع (go.sum، package-lock.json، Pipfile.lock، Gemfile.lock) — لا تحلَّ التبعيات في وقت التشغيل داخل CI.
  • تثبيت الصور الأساسية — استخدم digest محدد (FROM node:20@sha256:abc...) لا وسمًا متغيرًا (FROM node:latest).
  • طوابع زمنية حتمية — اضبط SOURCE_DATE_EPOCH (طابع Unix لآخر commit) حتى لا تُضمِّن أدوات الضغط والأرشفة وقت الساعة.
  • إصدارات أدوات ثابتة — حدِّد go-version: '1.23.4'، node-version: '20.11.0' بدقة، لا '20'.
الوسوم المتغيرة خطر إنتاجي. إذا كان نشرك يشير إلى myapp:latest ورفع شخص ما صورة معطوبة بنفس الوسم، فإن كل عملية نشر لاحقة ستسحب الصورة المعطوبة. انشر دائمًا بـ digest (myapp@sha256:...) أو بوسم SHA المنسوب إلى commit غير قابل للتغيير، وعامل latest كاسم مستعار للراحة في التطوير المحلي فقط.

توقيع المخرجات والمصادقة عليها

في شركات كـ Google، يُوقَّع كل مخرج بمفتاح تشفير (SLSA provenance). يتيح هذا للمستهلكين التحقق من أن المخرج صدر عن تشغيل خط أنابيب محدد من commit محدد — لا من مهاجم اخترق السجل. تدعم GitHub Actions هذا أصليًا عبر actions/attest-build-provenance، الذي يُصدر مصادقة SLSA المستوى 3 مخزنة في جذر الثقة لـ GitHub. هذا مطلوب بشكل متزايد من امتثال المؤسسات (NIST SSDF، قانون الصمود الإلكتروني الأوروبي).

أنماط الفشل الشائعة

  • تصادم أسماء المخرجات — مهمتان متوازيتان ترفعان مخرجًا بنفس الاسم؛ الثانية تُلغي الأولى بصمت. أدرج دائمًا متغير مصفوفة المهمة أو اسم الفرع في اسم المخرج.
  • غياب تبعية needs — تبدأ مهمة لاحقة قبل اكتمال رفع المخرج، فتحصل على 404 وتفشل بشكل عشوائي. أعلن دائمًا عن تبعيات needs صريحة.
  • رفع المستودع بأكمله — إعداد خاطئ path: . يرفع غيغابايتات ويُضخِّم تكاليف التخزين. كن محددًا: ارفع مجلد dist/ أو build/ فقط.
  • غياب سياسة الاحتفاظ — تكاليف تخزين المخرجات تتراكم. أتمت التنظيف بسياسات احتفاظ أو بمهمة تنظيف ليلية.