هل وجدت نفسك تنسخ وتلصق نفس الشيفرة عبر سياقات عمل GitHub متعددة؟ عندما تحتاج إلى أداء نفس المهمة في مستودعات أو سياقات عمل مختلفة، إن إنشاء إجراء GitHub مشترك هو الحل. في هذا البرنامج التعليمي، تعلم كيفية بناء إجراء JavaScript مخصص لـ GitHub من الصفر يمكنك مشاركته عبر منظمتك.
فهم إجراءات GitHub وسياقات العمل
قبل الانغماس في إنشاء إجراء مخصص، دعنا نرسخ بعض السياق. سياق عمل GitHub هو عملية آلية يمكنك إعدادها في مستودعك لبناء، اختبار، تعبئة، إصدار، أو نشر أي مشروع على GitHub. تتألف هذه السياقات من وظائف واحدة أو أكثر يمكنها تشغيلها تتابعيًا أو بشكل متوازي.
إجراءات GitHub هي المهام الفردية التي تشكل سياق عمل. فكر فيها كمكونات بناء قابلة لإعادة الاستخدام – تتعامل مع مهام محددة مثل فحص الشيفرة، تشغيل الاختبارات، أو نشرها على خادم. يوفر GitHub ثلاثة أنواع من الإجراءات:
- إجراءات حاوية Docker
- إجراءات JavaScript
- إجراءات مركبة
لهذا البرنامج التعليمي، سنركز على إنشاء إجراء JavaScript لأنه يعمل مباشرة على جهاز التشغيل ويمكنه التنفيذ بسرعة.
المشكلة: متى يجب إنشاء إجراء مخصص
لنستكشف متى ولماذا ترغب في إنشاء إجراء GitHub مخصص من خلال مثال عملي. طوال هذا البرنامج التعليمي، سنستخدم سيناريو محدد – تكامل مع خادم Devolutions (DVLS) لإدارة الأسرار – لتوضيح العملية، ولكن المفاهيم تنطبق على أي موقف تحتاج فيه إلى إنشاء إجراء مشترك قابل لإعادة الاستخدام.
💡 ملاحظة: إذا كان لديك خادم Devolutions (DVLS) وترغب في تخطي الجزء الخاص بالاستخدام، يمكنك العثور على النسخة المكتملة في مستودع Devolutions Github Actions.
تخيل أنك تدير عدة سير عمل على GitHub التي تحتاج إلى التفاعل مع خدمة خارجية – في مثالنا، استرداد الأسرار من DVLS. كل سير عمل يحتاج إلى هذه الوظيفة يتطلب الخطوات الأساسية نفسها:
- الاتصال بالخدمة الخارجية
- المصادقة
- تنفيذ العمليات المحددة
- معالجة النتائج
بدون إجراء مشترك، ستحتاج إلى تكرار هذا الكود في كل سير العمل. هذا ليس فقط غير كفؤ – بل أنه أصعب في الصيانة وأكثر عرضة للأخطاء.
لماذا إنشاء إجراء مشترك؟
إنشاء إجراء مشترك على GitHub يقدم العديد من الفوائد الرئيسية التي تنطبق على أي سيناريو للتكامل:
- إعادة استخدام الشيفرة: اكتب الشيفرة للتكامل مرة واحدة واستخدمها عبر عدة سير عمل ومستودعات
- الصيانة: قم بتحديث الإجراء في مكان واحد لنشر التغييرات في كل مكان يتم استخدامه
- التوحيد: تأكد من أن جميع الفرق تتبع نفس العملية للمهام الشائعة
- التحكم بالإصدار: تتبع التغييرات في الشيفرة للتكامل والتراجع عند الحاجة
- تقليل التعقيد: بسط سير العمل عن طريق تجريد تفاصيل التنفيذ
المتطلبات الأساسية
قبل بدء هذا البرنامج التعليمي، تأكد من توفر العناصر التالية:
- مستودع GitHub مع سير عمل موجود
- معرفة أساسية بGit، بما في ذلك استنساخ المستودعات وإنشاء الفروع
- صلاحيات مالك التنظيم لإنشاء وإدارة المستودعات المشتركة
- فهم أساسي للJavaScript وNode.js
في سيناريو العمل الخاص بنا، سنقوم بإنشاء إجراء يتكامل مع DVLS، ولكن يمكنك تكييف المفاهيم مع أي خدمة خارجية أو وظيفة مخصصة تحتاجها.
ما ستقوم بإنشائه
بحلول نهاية هذا البرنامج التعليمي، ستفهم كيفية:
- إنشاء مستودع GitHub عام للإجراءات المشتركة
- بناء عدة إجراءات متصلة (سنقوم بإنشاء اثنين كأمثلة):
- إحدى للتعامل مع المصادقة
- أخرى لأداء عمليات محددة
- إنشاء سير عمل يستخدم إجراءاتك المخصصة
سنقوم بتوضيح هذه المفاهيم من خلال بناء إجراءات تتكامل مع DVLS، ولكن يمكنك تطبيق نفس الأنماط لإنشاء إجراءات لأي غرض يحتاجه تنظيمك.
نقطة البداية: السير العمل الحالي
لنفحص سير عمل بسيط يُرسل إشعار Slack عند إنشاء إصدار جديد. يستخدم هذا السير العمل حاليًا أسرار GitHub لتخزين عنوان URL الويب الخاص بـ Slack:
name: Release Notification on: release: types: [published] jobs: notify: runs-on: ubuntu-latest steps: - name: Send Slack Notification run: | curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \\ -H "Content-Type: application/json" \\ --data '{ "text": "New release ${{ github.event.release.tag_name }} published!", "username": "GitHub Release Bot", "icon_emoji": ":rocket:" }'
لاحظ مرجع secrets.SLACK_WEBHOOK_URL
. يتم تخزين عنوان الويب هوك هذا حاليًا كسرّ سري لـ GitHub، ولكننا نريد استرداده من مثيل DVLS لدينا بدلاً من ذلك. في حين أن هذا مثال بسيط يستخدم سرًا واحدًا فقط، تخيل أن يكون لديك العشرات من سير العمل عبر منظمتك، كل منها يستخدم عدة أسرار. سيكون إدارة هذه الأسرار مركزيًا في DVLS بدلاً من أن تكون متناثرة في GitHub أكثر كفاءة بكثير.
خطة التنفيذ
لتحويل هذا السير العمل من استخدام أسرار GitHub إلى DVLS، نحتاج إلى:
- إعداد بيئة DVLS
- إنشاء الأسرار المقابلة في DVLS
- اختبار نقاط نهاية واجهة برمجة التطبيقات في DVLS للمصادقة واسترجاع السر
- إنشاء مستودع الإجراءات المشتركة
- بناء إجراء لمصادقة DVLS (
dvls-login
) - بناء إجراء لاسترجاع قيم الأسرار (
dvls-get-secret-entry
) - استخدام مترجم ncc الخاص بـ Vercel لربط الإجراءات بدون node_modules
- بناء إجراء لمصادقة DVLS (
- تعديل السير العمل
- استبدال مراجع الأسرار في GitHub بإجراءاتنا المخصصة
- اختبار التنفيذ الجديد
كل خطوة تعتمد على السابقة، وفي النهاية، ستمتلك حلاً قابلاً لإعادة الاستخدام يمكن لأي سير عمل في منظمتك الاستفادة منه. بينما نستخدم DVLS كمثال، يمكنك تكييف نفس النمط لأي خدمة خارجية تحتاج سير العمل الخاص بك للتفاعل معها.
الخطوة 1: استكشاف واجهة برمجة التطبيقات الخارجية
قبل إنشاء إجراء GitHub، تحتاج إلى فهم كيفية التفاعل مع خدمتك الخارجية. بالنسبة لمثالنا DVLS، نحتاج إلى وجود سريتين مكونتين مسبقًا في نسخة DVLS:
DVLS_APP_KEY
– مفتاح التطبيق للمصادقةDVLS_APP_SECRET
– السر الخاص بالتطبيق للمصادقة
اختبار تدفق واجهة البرمجة التطبيقية
لنستخدم PowerShell لاستكشاف واجهة برمجة التطبيقات في DVLS وفهم التدفق الذي سنحتاج إلى تنفيذه في إجراءنا. هذه المرحلة الاستكشافية أمر حاسم عند إنشاء أي إجراء مخصص – تحتاج إلى فهم متطلبات واجهة البرمجة التطبيقية قبل تنفيذها.
$dvlsUrl = '<https://1.1.1.1/dvls>' $appId = 'xxxx' $appSecret = 'xxxxx' # Step 1: Authentication $loginResult = Invoke-RestMethod -Uri "$dvlsUrl/api/v1/login" ` -Body @{ 'appKey' = $appId 'appSecret' = $appSecret } ` -Method Post ` -SkipCertificateCheck # Step 2: Get Vault Information $vaultResult = Invoke-RestMethod -Uri "$dvlsUrl/api/v1/vault" ` -Headers @{ 'tokenId' = $loginResult.tokenId } ` -SkipCertificateCheck $vault = $vaultResult.data.where({$_.name -eq 'DevOpsSecrets'}) # Step 3: Get Entry ID $entryResponse = Invoke-RestMethod -Uri "$dvlsUrl/api/v1/vault/$($vault.id)/entry" ` -Headers @{ tokenId = $loginResult.tokenId } ` -Body @{ name = 'azure-acr' } ` -SkipCertificateCheck # Step 4: Retrieve Secret Value $passwordResponse = Invoke-RestMethod -Uri "$dvlsUrl/api/v1/vault/$($vault.id)/entry/$($entryResponse.data[0].id)" ` -Headers @{ tokenId = $loginResult.tokenId } ` -Body @{ includeSensitiveData = $true } ` -SkipCertificateCheck $passwordResponse.data.password
هذا الاستكشاف يكشف عن تدفق واجهة البرمجة التطبيقية الذي سنحتاج إلى تنفيذه في إجراء GitHub الخاص بنا:
- المصادقة مع DVLS باستخدام بيانات التطبيق
- الحصول على معلومات الخزان باستخدام الرمز المسترد
- تحديد معرف الإدخال المحدد لسرنا
- استرداد قيمة السر الفعلية
فهم هذا التدفق أمر حاسم لأنه سيتعين علينا تنفيذ نفس الخطوات في إجراء GitHub الخاص بنا، مستخدمين JavaScript بدلاً من PowerShell.
عند إنشاء إجراءك المخصص الخاص، ستتبع عملية مماثلة:
- تحديد نقاط نهاية واجهة برمجة التطبيقات التي تحتاج للتفاعل معها
- اختبار عملية المصادقة واسترداد البيانات
- توثيق الخطوات التي ستحتاج إلى تنفيذها في إجراءك
الخطوة ٢: إنشاء إجراء المصادقة
الآن بعد أن فهمنا تدفق واجهة برمجة التطبيقات، دعنا ننشئ إجراءنا المخصص الأول لمعالجة عملية المصادقة. سنقوم ببناء هذا في مستودع مشترك جديد.
إعداد هيكل الإجراء
أولاً، قم بإنشاء هيكل الملفات التالي في مستودعك:
dvls-actions/ ├── login/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md
هذا الهيكل الملفاتي منظم لإنشاء إجراء GitHub قابل للتوسيع والصيانة:
- login/ – دليل مخصص لإجراء المصادقة، يحافظ على تجميع الملفات ذات الصلة معًا
- index.js – الشيفرة الرئيسية للإجراء التي تحتوي على منطق المصادقة والتفاعل مع واجهة برمجة التطبيقات
- action.yml – يحدد واجهة الإجراء، بما في ذلك المدخلات المطلوبة وكيفية تشغيل الإجراء
- package.json – يدير التبعيات وبيانات المشروع
- README.md – توثيق لمستخدمي الإجراء
هذا الهيكل يتبع أفضل الممارسات لإجراءات GitHub، مما يحافظ على تنظيم الشيفرة ويجعله سهل الصيانة والتحديث مع مرور الوقت.
إنشاء شيفرة الإجراء
أولاً، يجب عليك إنشاء شيفرة الإجراء. وهذا ينطوي على إنشاء الملف الرئيسي JavaScript الذي سيدير منطق المصادقة:
- إنشاء
index.js
– هنا يعيش منطق الإجراء الرئيسي:
// Required dependencies // @actions/core - GitHub Actions toolkit for input/output operations const core = require('@actions/core'); // axios - HTTP client for making API requests const axios = require('axios'); // https - Node.js HTTPS module for SSL/TLS support const https = require('https'); // Create an axios instance with SSL verification disabled // This is useful when dealing with self-signed certificates const axiosInstance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false }) }); /** * Authenticates with the Devolutions Server and retrieves an auth token * @param {string} serverUrl - The base URL of the Devolutions Server * @param {string} appKey - Application key for authentication * @param {string} appSecret - Application secret for authentication * @returns {Promise<string>} The authentication token */ async function getAuthToken(serverUrl, appKey, appSecret) { core.info(`Attempting to get auth token from ${serverUrl}/api/v1/login`); const response = await axiosInstance.post(`${serverUrl}/api/v1/login`, { appKey: appKey, appSecret: appSecret }); core.info('Successfully obtained auth token'); return response.data.tokenId; } /** * Wrapper function for making HTTP requests with detailed error handling * @param {string} description - Description of the request for logging * @param {Function} requestFn - Async function that performs the actual request * @returns {Promise<any>} The result of the request * @throws {Error} Enhanced error with detailed debugging information */ async function makeRequest(description, requestFn) { try { core.info(`Starting request: ${description}`); const result = await requestFn(); core.info(`Successfully completed request: ${description}`); return result; } catch (error) { // Detailed error logging for debugging purposes core.error('=== Error Details ==='); core.error(`Error Message: ${error.message}`); core.error(` core.error(`Status Text: ${error.response?.statusText}`); // Log response data if available if (error.response?.data) { core.error('Response Data:'); core.error(JSON.stringify(error.response.data, null, 2)); } // Log request configuration details if (error.config) { core.error('Request Details:'); core.error(`URL: ${error.config.url}`); core.error(`Method: ${error.config.method}`); core.error('Request Data:'); core.error(JSON.stringify(error.config.data, null, 2)); } core.error('=== End Error Details ==='); // Throw enhanced error with API message if available const apiMessage = error.response?.data?.message; throw new Error(`${description} failed: ${apiMessage || error.message} ( } } /** * Main execution function for the GitHub Action * This action authenticates with a Devolutions Server and exports the token * for use in subsequent steps */ async function run() { try { core.info('Starting Devolutions Server Login action'); // Get input parameters from the workflow const serverUrl = core.getInput('server_url'); const appKey = core.getInput('app_key'); const appSecret = core.getInput('app_secret'); const outputVariable = core.getInput('output_variable'); core.info(`Server URL: ${serverUrl}`); core.info('Attempting authentication...'); // Authenticate and get token const token = await makeRequest('Authentication', () => getAuthToken(serverUrl, appKey, appSecret) ); // Mask the token in logs for security core.setSecret(token); // Make token available as environment variable core.exportVariable(outputVariable, token); // Set token as output for other steps core.setOutput('token', token); core.info('Action completed successfully'); } catch (error) { // Handle any errors that occur during execution core.error(`Action failed: ${error.message}`); core.setFailed(error.message); } } // Execute the action run();
يستخدم الكود حزمة @actions/core
من toolkit GitHub للتعامل مع المدخلات والمخرجات وتسجيل الأحداث. لقد قمنا أيضًا بتنفيذ معالجة أخطاء قوية وتسجيل لجعل عملية التصحيح أسهل.
لا تقلق كثيرًا بشأن فهم تفاصيل جميع رموز جافا سكريبت هنا! النقطة الرئيسية هي أن كود GitHub Action هذا يحتاج فقط إلى فعل شيء رئيسي واحد: استخدام core.setOutput()
لإرجاع رمز المصادقة.
إذا كنت غير مرتاح في كتابة هذا الجافا سكريبت بنفسك، يمكنك استخدام أدوات مثل ChatGPT للمساعدة في إنشاء الكود. الجزء الأهم هو فهم أن الإجراء يحتاج إلى:
- الحصول على قيم المدخلات (مثل عنوان URL للخادم وبيانات الاعتماد)
- إجراء طلب المصادقة
- إرجاع الرمز باستخدام
core.setOutput()
إنشاء حزمة NodeJS
الآن بعد فهم هيكل الكود ووظائف إجراءنا، دعنا نقوم بإعداد تكوين حزمة Node.js. وينطوي ذلك على إنشاء ملفات الحزمة الضرورية وتثبيت التبعيات التي ستحتاجها الإجراء للعمل بشكل صحيح.
- إنشاء
package.json
لتحديد تبعياتنا وبيانات أخرى حول الإجراءات.{ "name": "devolutions-server-login", "version": "1.0.0", "description": "إجراء GitHub للمصادقة على خادم Devolutions", "main": "index.js", "scripts": { "test": "echo \\"Error: no test specified\\" && exit 1" }, "keywords": [ "devolutions_server" ], "author": "Adam Bertram", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", "axios": "^1.6.7" } }
- تثبيت التبعيات عن طريق تشغيل
npm install
.npm install
بعد تثبيت التبعيات، يجب رؤية دليل
node_modules
الجديد الذي تم إنشاؤه في مجلد المشروع الخاص بك. يحتوي هذا الدليل على جميع الحزم المطلوبة التي يحتاجها الإجراء الخاص بك للتشغيل.ملاحظة: بينما سنقوم بتأكيد
package.json
وpackage-lock.json
إلى التحكم بالإصدار، سنستبعد في النهاية دليلnode_modules
باستخدامncc
لربط تبعياتنا. - أنشئ
action.yml
لتعريف واجهة الإجراء:name: 'تسجيل الدخول إلى خادم Devolutions' description: 'المصادقة والحصول على رمز من خادم Devolutions' inputs: server_url: description: 'عنوان URL لخادم Devolutions' required: true app_key: description: 'مفتاح التطبيق للمصادقة' required: true app_secret: description: 'سر التطبيق للمصادقة' required: true output_variable: description: 'اسم المتغير البيئي لتخزين الرمز المُسترجع' required: false default: 'DVLS_TOKEN' runs: using: 'node20' main: 'index.js'
يعتبر ملف
action.yml
أمرًا حاسمًا حيث يحدد وظيفة الإجراء داخل سير العمل الخاص بـ GitHub Actions. دعنا نفصل مكوناته الرئيسية:- الاسم والوصف: يوفران معلومات أساسية حول ما يقوم به الإجراء الخاص بك
- inputs: يحدد المعلمات التي يمكن للمستخدمين تمريرها إلى الإجراء الخاص بك:
server_url
: حيث يمكن العثور على خادم Devolutionsapp_key
وapp_secret
: بيانات اعتماد المصادقةoutput_variable
: حيث يتم تخزين الرمز الناتج
- runs: يحدد كيفية تنفيذ الإجراء:
using: 'node20'
: يستخدم إصدار Node.js 20main: 'index.js'
: يشير إلى الملف الرئيسي لجافا سكريبت
عندما يشير المستخدمون إلى هذا الإجراء في سير العمل الخاص بهم، سيقدمون هذه المدخلات وفقًا لتعريف هذه الواجهة.
تحسين الإجراء
لجعل إجراءنا أكثر صيانة وكفاءة، سنستخدم مترجم ncc
من Vercel لربط جميع التبعيات في ملف واحد. وهذا يلغي الحاجة إلى تأكيد دليل node_modules
:
من غير المستحسن تضمين node_modules في مستودع إجراء GitHub الخاص بك لأسباب عدة:
- يمكن أن يكون دليل node_modules كبيرًا جدًا، حيث يحتوي على جميع التبعيات وتبعياتها الفرعية، مما قد يؤدي إلى زيادة حجم المستودع بشكل غير ضروري
- قد تتعامل أنظمة التشغيل والبيئات المختلفة مع node_modules بشكل مختلف، مما قد يتسبب في مشاكل في التوافق
- استخدام مترجم Vercel’s ncc لجمع جميع التبعيات في ملف واحد هو نهج أفضل لأنه:
- يخلق إجراءً أكثر كفاءة وقابلية للصيانة
- يحرر الحاجة إلى الالتزام بدليل node_modules
- تثبيت
ncc
:npm i -g @vercel/ncc
- بناء النسخة المجمعة:
ncc build index.js --license licenses.txt
- تحديث
action.yml
للإشارة إلى الملف المجمّع:runs: using: 'node20' main: 'dist/index.js' # تم تحديثه لاستخدام النسخة المجمعة
- تنظيف:
rm -rf node_modules # إزالة دليل node_modules
- التزام الملفات في المستودع المشترك.
git add . git commit -m "الالتزام الأول لإجراء تسجيل دخول DVLS" git push
إنشاء README
الجميع يحب الوثائق، أليس كذلك؟ لا؟ حسنًا، أنا أيضًا لا أحب ذلك، لذا قمت بإنشاء قالب README لاستخدامك. تأكد من ملء هذا وإضافته مع الإجراء الخاص بك.
# GitHub Action Template This template provides a standardized structure for documenting any GitHub Action. Replace the placeholders with details specific to your action. --- # Action Name A brief description of what this GitHub Action does. ## Prerequisites Outline any setup or configuration required before using the action. For example:
الخطوات:
- name: خطوة مسبقة
uses: example/action-name@v1
with:
inputname: ${{ secrets.INPUTSECRET }}
## Inputs | Input Name | Description | Required | Default | |-------------------|------------------------------------------------|----------|----------------| | `input_name` | Description of the input parameter | Yes/No | Default Value | | `another_input` | Description of another input parameter | Yes/No | Default Value | ## Outputs | Output Name | Description | |-------------------|------------------------------------------------| | `output_name` | Description of the output parameter | | `another_output` | Description of another output parameter | ## Usage Provide an example of how to use this action in a workflow:
steps:
- اسم: اسم الخطوة
الاستخدامات: your-org/action-name@v1
مع:
الإدخالالاسم: ‘قيمة الإدخال’
آخرالإدخال: ‘قيمة أخرى’
## Example Workflow Here's a complete example workflow utilizing this action:
الاسم: مثال على سير العمل
عند: [دفع]
الوظائف:
مثال-وظيفة:
تشغيل على: ubuntu-latest
الخطوات:
– الاسم: التحقق من المستودع
الاستخدامات: actions/checkout@v3
- name: Run Action uses: your-org/action-name@v1 with: input_name: 'Input Value' another_input: 'Another Value' - name: Use Output run: | echo "Output value: ${{ steps.step_id.outputs.output_name }}"
## Security Notes - Highlight best practices for using sensitive data, such as storing secrets in GitHub Secrets. - Remind users not to expose sensitive information in logs. ## License Include the license details for this action, e.g., MIT License: This GitHub Action is available under the [MIT License](LICENSE).
نقاط رئيسية يجب تذكرها
عند إنشاء إجراء مخصص خاص بك:
- قم دائمًا بتنفيذ معالجة الأخطاء والتسجيل بشكل شامل
- استخدم حزمة
@actions/core
للتكامل الصحيح مع GitHub Actions - قم بتجميع الاعتمادات باستخدام
ncc
للحفاظ على نظافة المستودع - وثق المدخلات والمخرجات بوضوح في
action.yml
- اعتبر تداعيات الأمان وقم بإخفاء القيم الحساسة باستخدام
core.setSecret()
سيتم استخدام هذا الإجراء الخاص بالتحقق من الهوية من قبل الإجراء التالي الذي يسترجع الأسرار. دعنا ننتقل إلى إنشاء ذلك الإجراء.
الخطوة 3: إنشاء إجراء “الحصول على السر”
لقد قمت بالعمل الشاق حتى هذه النقطة. أنت الآن تعرف كيفية إنشاء إجراء مخصص في GitHub. إذا كنت تتبع، فأنت الآن بحاجة إلى تكرار تلك الخطوات لإجراء إدخال سر DVLS كما يلي:
هيكل الإجراء
dvls-actions/ ├── get-secret-entry/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md
ملف index.js
// Required dependencies const core = require('@actions/core'); // GitHub Actions toolkit for action functionality const axios = require('axios'); // HTTP client for making API requests const https = require('https'); // Node.js HTTPS module for SSL/TLS support // Create an axios instance that accepts self-signed certificates const axiosInstance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false }) }); /** * Retrieves the vault ID for a given vault name from the DVLS server * @param {string} serverUrl - The URL of the DVLS server * @param {string} token - Authentication token for API access * @param {string} vaultName - Name of the vault to find * @returns {string|null} - Returns the vault ID if found, null otherwise */ async function getVaultId(serverUrl, token, vaultName) { core.debug(`Attempting to get vault ID for vault: ${vaultName}`); const response = await axiosInstance.get(`${serverUrl}/api/v1/vault`, { headers: { tokenId: token } }); core.debug(`Found ${response.data.data.length} vaults`); // Find the vault with matching name const vault = response.data.data.find(v => v.name === vaultName); if (vault) { core.debug(`Found vault ID: ${vault.id}`); } else { // Log available vaults for debugging purposes core.debug(`Available vaults: ${response.data.data.map(v => v.name).join(', ')}`); } return vault ? vault.id : null; } /** * Retrieves the entry ID for a given entry name within a vault * @param {string} serverUrl - The URL of the DVLS server * @param {string} token - Authentication token for API access * @param {string} vaultId - ID of the vault containing the entry * @param {string} entryName - Name of the entry to find * @returns {string} - Returns the entry ID * @throws {Error} - Throws if entry is not found */ async function getEntryId(serverUrl, token, vaultId, entryName) { core.debug(`Attempting to get entry ID for entry: ${entryName} in vault: ${vaultId}`); const response = await axiosInstance.get( `${serverUrl}/api/v1/vault/${vaultId}/entry`, { headers: { tokenId: token }, data: { name: entryName }, params: { name: entryName } } ); const entryId = response.data.data[0].id; if (!entryId) { // Log full response for debugging if entry not found core.debug('Response data:'); core.debug(JSON.stringify(response.data, null, 2)); throw new Error(`Entry '${entryName}' not found`); } core.debug(`Found entry ID: ${entryId}`); return entryId; } /** * Retrieves the password for a specific entry in a vault * @param {string} serverUrl - The URL of the DVLS server * @param {string} token - Authentication token for API access * @param {string} vaultId - ID of the vault containing the entry * @param {string} entryId - ID of the entry containing the password * @returns {string} - Returns the password */ async function getPassword(serverUrl, token, vaultId, entryId) { core.debug(`Attempting to get password for entry: ${entryId} in vault: ${vaultId}`); const response = await axiosInstance.get( `${serverUrl}/api/v1/vault/${vaultId}/entry/${entryId}`, { headers: { tokenId: token }, data: { includeSensitiveData: true }, params: { includeSensitiveData: true } } ); core.debug('Successfully retrieved password'); return response.data.data.password; } /** * Generic request wrapper with enhanced error handling and debugging * @param {string} description - Description of the request for logging * @param {Function} requestFn - Async function containing the request to execute * @returns {Promise<any>} - Returns the result of the request function * @throws {Error} - Throws enhanced error with API response details */ async function makeRequest(description, requestFn) { try { core.debug(`Starting request: ${description}`); const result = await requestFn(); core.debug(`Successfully completed request: ${description}`); return result; } catch (error) { // Log detailed error information for debugging core.debug('Full error object:'); core.debug(JSON.stringify({ message: error.message, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, headers: error.response?.headers, url: error.config?.url, method: error.config?.method, requestData: error.config?.data, queryParams: error.config?.params }, null, 2)); const apiMessage = error.response?.data?.message; throw new Error(`${description} failed: ${apiMessage || error.message} ( } } /** * Main execution function for the GitHub Action * Retrieves a password from DVLS and sets it as an output/environment variable */ async function run() { try { core.debug('Starting action execution'); // Get input parameters from GitHub Actions const serverUrl = core.getInput('server_url'); const token = core.getInput('token'); const vaultName = core.getInput('vault_name'); const entryName = core.getInput('entry_name'); const outputVariable = core.getInput('output_variable'); core.debug(`Server URL: ${serverUrl}`); core.debug(`Vault Name: ${vaultName}`); core.debug(`Entry Name: ${entryName}`); // Sequential API calls to retrieve password const vaultId = await makeRequest('Get Vault ID', () => getVaultId(serverUrl, token, vaultName) ); if (!vaultId) { throw new Error(`Vault '${vaultName}' not found`); } const entryId = await makeRequest('Get Entry ID', () => getEntryId(serverUrl, token, vaultId, entryName) ); const password = await makeRequest('Get Password', () => getPassword(serverUrl, token, vaultId, entryId) ); // Set the password as a secret and output core.setSecret(password); // Mask password in logs core.exportVariable(outputVariable, password); // Set as environment variable core.setOutput('password', password); // Set as action output core.debug('Action completed successfully'); } catch (error) { core.debug(`Action failed: ${error.message}`); core.setFailed(error.message); } } // Execute the action run();
Package.json
{ "name": "devolutions-server-get-entry", "version": "1.0.0", "description": "GitHub Action to retrieve entries from Devolutions Server", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "devolutions_server" ], "author": "Adam Bertram", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", "axios": "^1.6.7" } }
Action.yml
name: 'Devolutions Server Get SecretEntry' description: 'Authenticate and get a secret entry from Devolutions Server' inputs: server_url: description: 'URL of the Devolutions Server' required: true token: description: 'Token for authentication' required: true vault_name: description: 'Name of the vault containing the secret entry' required: true entry_name: description: 'Name of the secret entry to retrieve' required: true output_variable: description: 'Name of the environment variable to store the retrieved secret' required: false default: 'DVLS_ENTRY_SECRET' runs: using: 'node20' main: 'index.js'
تحسين الإجراء
- قم بترجمة ملف الفهرس.
npm i -g @vercel/ncc ncc build index.js --license licenses.txt
- قم بتحديث
action.yml
لتشير إلى الملف المجمع:runs: using: 'node20' main: 'dist/index.js' # تم التحديث لاستخدام النسخة المجمعة
- تنظيف:
rm -rf node_modules # إزالة دليل node_modules
- قم بتنزيل الملفات إلى مستودع المشاركة.
git add . git commit -m "التزام أولي لإجراء إدخال سري لـ DVLS" git push
النتيجة النهائية
في هذه النقطة، يجب أن يكون لديك مستودعي GitHub:
- المستودع الذي يحتوي على سير العمل الذي كنت تستخدم فيه الأسرار على GitHub
- المستودع المشترك (بشرط أن يكون الاسم dvls-actions) الذي يحتوي على الإجراءين بنية تبدو كالتالي:
dvls-actions/ ├── login/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md ├── get-secret-entry/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md
استخدام الإجراءات المخصصة
بمجرد إعداد هذه الإجراءات المخصصة، يمكنك استخدامها في سير العمل الأصلي الذي تقوم بالاستدعاء فيه.
سير العمل الأصلي:
- يستخدم خطوة واحدة لإرسال إشعار Slack
- يشير مباشرة إلى عنوان URL للويبهوك من الأسرار (
secrets.SLACK_WEBHOOK_URL
)
سير العمل الجديد:
- يضيف خطوة مصادقة باستخدام إجراء تسجيل الدخول المخصص لـ DVLS
- يسترد عنوان URL الآمن للويبهوك من خادم Devolutions
- يستخدم متغيرات البيئة بدلاً من الأسرار
- يحافظ على نفس وظيفة الإشعار ولكن مع تعزيز الأمان
تضيف سير العمل الجديد اثنين من الخطوات قبل إشعار Slack:
- المصادقة مع خادم Devolutions باستخدام إجراء
dvls-login
- استرداد عنوان URL للويبهوك في Slack باستخدام إجراء
dvls-get-secret-entry
- تبقى خطوة الإشعار في Slack النهائية مماثلة ولكن تستخدم عنوان URL للويبهوك الذي تم استرجاعه من متغير البيئة (
env.SLACK_WEBHOOK_URL
)
name: Release Notification on: release: types: [published] jobs: notify: runs-on: ubuntu-latest steps: - name: Login to Devolutions Server uses: devolutions-community/dvls-login@main with: server_url: 'https://1.1.1.1/dvls' app_key: ${{ vars.DVLS_APP_KEY }} app_secret: ${{ vars.DVLS_APP_SECRET }} - name: Get Slack Webhook URL uses: devolutions-community/dvls-get-secret-entry@main with: server_url: 'https://1.1.1.1/dvls' token: ${{ env.DVLS_TOKEN }} vault_name: 'DevOpsSecrets' entry_name: 'slack-webhook' output_variable: 'SLACK_WEBHOOK_URL' - name: Send Slack Notification run: | curl -X POST ${{ env.SLACK_WEBHOOK_URL }} \ -H "Content-Type: application/json" \ --data '{ "text": "New release ${{ github.event.release.tag_name }} published!", "username": "GitHub Release Bot", "icon_emoji": ":rocket:" }'
إنشاء إجراءات GitHub المخصصة يسمح لك بتوحيد وتأمين سير العمل الخاصة بك عبر مستودعات متعددة. من خلال نقل العمليات الحساسة مثل المصادقة واسترجاع السر إلى إجراءات مخصصة، يمكنك:
- الحفاظ على ممارسات أمان أفضل من خلال تركيز إدارة الاعتمادات
- تقليل تكرار الكود عبر سير العمل المختلفة
- تبسيط صيانة سير العمل وتحديثاته
- ضمان تنفيذ متسق للعمليات الحرجة
يوضح مثال تكامل خادم Devolutions مع إجراءات GitHub كيف يمكن للإجراءات المخصصة أن تجسر الفجوة بين أدوات مختلفة مع الحفاظ على ممارسات الأمان الأفضل. يمكن تكييف هذا النهج لمختلف التكاملات الأخرى وحالات الاستخدام في سير العمل الخاصة بك في DevOps.
Source:
https://adamtheautomator.com/custom-github-actions-guide/