האם מצאת עצמך מעתיק ומדביק אותו קוד במהלך עבודה במספר זרים שונים ב-GitHub? כאשר נדרש לבצע אותה משימה במאגרי קוד שונים או בתהליכי עבודה שונים, יצירת פעולת GitHub משותפת היא הדרך הנכונה לפתור את הבעיה. במדריך זה, למד כיצד לבנות פעולת JavaScript מותאמת אישית ב-GitHub Action מאפס שתוכל לשתף בכל הארגון שלך.
הבנת פעולות ותהליכי עבודה ב-GitHub
לפני שנכנסים ליצירת פעולה מותאמת, נקבע קצת הקשר. תהליך עבודה ב-GitHub הוא תהליך אוטומטי שניתן להגדיר במאגר הקוד שלך כדי לבנות, לבדוק, לארוז, לשחרר או להפיץ כל פרויקט ב-GitHub. תהליכי העבודה אלה מורכבים מפעולה אחת או יותר שיכולות לרוץ סידרתית או במקביל.
פעולות GitHub הן המשימות הפרטיות שמרכיבות תהליך עבודה. חשב עליהן כעל בלוקים בנייה שניתן להשתמש בהם שוב ושוב – הן מטפלות במשימות ספציפיות כמו בדיקת קוד, הרצת בדיקות או התקנה על שרת. GitHub מספקת שלושה סוגים של פעולות:
- פעולות תאי Docker
- פעולות JavaScript
- פעולות מרוכבות
במדריך זה, נתמקד ביצירת פעולת JavaScript מאחר שהיא רצה ישירות על המכונה המבצעת ויכולה להריץ במהירות.
הבעיה: מתי ליצור פעולה מותאמת אישית
בואו נבדוק מתי ולמה תרצו ליצור פעולת GitHub מותאמת אישית דרך דוגמה מעשית. במהלך המדריך זה, נשתמש בתרחיש ספציפי – שילוב עם שרת Devolutions (DVLS) לניהול סודות – על מנת להדגים את התהליך, אך המושגים יכולים להיות רלוונטיים לכל מצב בו נדרשת פעולה משותפת ומשתמשת.
💡 שימו לב: אם יש לכם שרת Devolutions (DVLS) ורוצים לדלג לחלק השימוש, אתם יכולים למצוא את הגרסה המושלמת ב-מאגר פעולות GitHub של Devolutions.
דמיינו שאתם מנהלים מספר זרימות עבודה של GitHub שצריכות לתקשר עם שירות חיצוני – בדוגמה שלנו, שליפת סודות מ-DVLS. כל זרימת עבודה שזקוקה לפונקציה הזו דורשת את אותם צעדים בסיסיים:
- חיבור לשירות החיצוני
- אימות
- ביצוע פעולות מסוימות
- טיפול בתוצאות
בלי פעולה משותפת, תצטרכו לה duplicar את הקוד הזה בכל זרימת עבודה. זה לא רק לא יעיל – זה גם קשה יותר לתחזוקה ונוטה יותר לשגיאות.
למה ליצור פעולה משותפת?
יצירת פעולה משותפת של GitHub מציעה מספר יתרונות מרכזיים החלים על כל תרחיש אינטגרציה:
- שימוש חוזר בקוד: כתבו את קוד האינטגרציה פעם אחת והשתמשו בו בכל הזרימות עבודה והמאגרות
- תחזוקה: עדכנו את הפעולה במקום אחד כדי להוציא שינויים בכל מקום שבו היא נמצאת
- סטנדרטיזציה: ודאו שכל הצוותים פועלים לפי אותו תהליך למשימות משותפות
- שליטת גרסאות: עקבו אחרי שינויים בקוד האינטגרציה והחזירו אם צריך
- צמצום מורכבות: הפכו את הזרימות לע simples על ידי הפשטת פרטי ההיישום
דרישות קדם
قبل שתתחיל את המדריך הזה, ודא שיש לך את הדברים הבאים:
- מאגר GitHub עם זרימת עבודה קיימת
- ידע בסיסי ב-Git, כולל שכפול מאגרים ויצירת סניפים
- גישה כבעל ארגון כדי ליצור ולנהל מאגרים משותפים
- הבנה בסיסית של JavaScript ו-Node.js
בדוגמת המקרה שלנו, ניצור פעולה שמשתלבת עם DVLS, אך תוכל להתאים את המושגים לכל שירות חיצוני או פונקציה מותאמת שאתה זקוק לה.
מה תיצור
בסוף המדריך הזה, תבין כיצד:
- ליצור מאגר GitHub ציבורי לפעולות משותפות
- לבנות מספר פעולות מקושרות (ניצור שתיים כדוגמאות):
- אחת לטיפול באותנטיקציה
- אחרת לביצוע פעולות ספציפיות
- ליצור זרימת עבודה שמשתמשת בפעולות המותאמות שלך
נציג את המושגים הללו על ידי בניית פעולות שמשתלבות עם DVLS, אך תוכל ליישם את אותם תבניות כדי ליצור פעולות לכל מטרה שהארגון שלך זקוק לה.
נקודת התחלה: זרימת העבודה הקיימת
בוא נבחן זרימת עבודה פשוטה ששולחת הודעת Slack כאשר משתחררת גרסה חדשה. זרימת עבודה זו משתמשת כרגע בסודות של GitHub כדי לאחסן את כתובת ה-URL של Slack webhook:
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
. כתובת ה-Webhook זו כרגע מאוחסנת כסוד ב-GitHub, אך אנו רוצים לקבל אותה מ-DVLS שלנו במקום. בעוד זו דוגמה פשוטה המשתמשת רק בסוד אחד, דמיין יותר מאותו עבודות כתפים בארגונך, כל אחת משתמשת במספר סודות. ניהול הסודות האלה במרכז ב-DVLS במקום לפזור אותם בכל תפריטי GitHub יהיה יעיל יותר.
תוכנית היישום
כדי להמיר את זרימת העבודה הזו משימוש בסודות של GitHub ל-DVLS, אנו צריכים ל:
- להכין את סביבת ה-DVLS
- ליצור סודות תואמים ב-DVLS
- לבדוק את נקודות ה-API של DVLS לאימות ולגישה לסוד
- ליצור את מאגר הפעולות המשותף
- לבנות פעולה לאימות של DVLS (
dvls-login
) - לבנות פעולה לקבלת ערכי סוד (
dvls-get-secret-entry
) - להשתמש במהדר ncc של Vercel כדי לארוז את הפעולות ללא node_modules
- לבנות פעולה לאימות של DVLS (
- לשנות את זרימת העבודה
- להחליף את ההפניות לסודות של GitHub בפעולות המותאמות שלנו
- לבדוק את היישום החדש
כל שלב בונה על הקודם, ועד סיום, תהיה לך פתרון ניתן לשימוש שכל תהליך בארגונך יכול להשתמש בו. בזמן שאנו משתמשים ב-DVLS כדוגמה, תוכל להתאים את אותו דפוס גם לשירות חיצוני אחר שצריך תקשורת עימו בתהליכי העבודה שלך.
שלב 1: חקירת ה- API החיצוני
לפני שתיצור פעולת GitHub, עליך להבין כיצד לתקשר עם השירות החיצוני שלך. לדוגמה שלנו מ-DVLS, אנו צריכים שני סודות שכבר הוגדרו במופע ה-DVLS:
DVLS_APP_KEY
– מפתח היישום לאימותDVLS_APP_SECRET
– הסוד היישומי לאימות
בדיקת זרימת ה- API
בואו נשתמש ב-PowerShell כדי לחקור את ה- API של DVLS ולהבין את הזרימה שעלינו ליישם בפעולתנו. שלב זה של חקירה הוא קריטי ביצירת כל פעולה מותאמת – עליך להבין את דרישות ה- API לפני ליישום אותן.
$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
חקירה זו חושפת את זרימת ה- API שעלינו ליישם בפעולת GitHub שלנו:
- אימות עם DVLS באמצעות פרטי היישום
- קבלת מידע על הארון באמצעות האסימון שחזר
- איתור מזהה הרשומה הספציפית של הסוד שלנו
- אחזור ערך הסוד האמיתי
הבנת הזרימה הזו חיונית מכיוון שעלינו ליישם את אותם שלבים בפעולת GitHub שלנו, רק על ידי שימוש ב-JavaScript במקום PowerShell.
כאשר אתה יוצר פעולת מותאמת אישית שלך, תעקוב אחר תהליך דומה:
- זיהוי נקודות הקצה של ה-API שצריך לתקשר איתן
- בדוק את תהליך האימות והבאת הנתונים
- תעד את הצעדים שתצטרך ליישם בפעולה שלך
שלב 2: יצירת פעולת האימות
עכשיו כשאנחנו מבינים את זרימת ה-API, בואו ניצור את הפעולה המותאמת הראשונה שלנו לטיפול באימות. נבנה את זה במאגר משותף חדש.
הגדרת מבנה הפעולה
ראשית, צור את מבנה הקבצים הבא במאגר שלך:
dvls-actions/ ├── login/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md
מבנה קבצים זה מאורגן כדי ליצור פעולה מודולרית ונתמכת ב-GitHub:
- login/ – תיקייה ייעודית לפעולת האימות, שומרת קבצים קשורים יחד
- index.js – קוד הפעולה הראשי שמכיל את הלוגיקה של האימות ואינטראקציות עם ה-API
- 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
מתוך ערכת הכלים של GitHub כדי לטפל בקלטים, פלטים ולוגים. כמו כן, עיצבנו טיפול בשגיאות ולוגים איכותי כדי להקל על תהליך האיתור והתיקון של בעיות.
אל תדאגו יתר על המידע הטכני של קוד JavaScript כאן! הנקודה המרכזית היא שקוד פעולת GitHub Action זה צריך רק לעשות דבר בסיסי: להשתמש ב־core.setOutput()
כדי להחזיר את טוקן האימות.
אם אינכם נוחים לכתוב את הקוד JavaScript בעצמכם, ניתן להשתמש בכלים כמו ChatGPT לעזרה ביצירת הקוד. החלק החשוב ביותר הוא להבין שהפעולה צריכה:
- לקבל את ערכי הקלט (כמו כתובת השרת ופרטי הכניסה)
- לבצע את בקשת האימות
- להחזיר את הטוקן באמצעות
core.setOutput()
יצירת חבילת NodeJS
עכשיו שהבנו את מבנה הקוד והפונקציונליות של הפעולה שלנו, בואו נגדיר את התצורה של חבילת ה־Node.js. זה כולל את יצירת קבצי החבילה הנחוצים והתקנת התלות שהפעולה שלנו תצטרך כדי לפעול בצורה תקינה.
- צור
package.json
כדי להגדיר את התלויות שלנו ומטה-נתונים אחרים.{ "name": "devolutions-server-login", "version": "1.0.0", "description": "GitHub Action לאימות לשרת 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 Server Login' 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. בואו נפרק את הרכיבים העיקריים שלו:- name ו-description: מספקים מידע בסיסי על מה עושה הפעולה שלך
- inputs: מגדיר את הפרמטרים שמשתמשים יכולים להעביר לפעולה שלך:
server_url
: היכן למצוא את שרת Devolutionsapp_key
ו-app_secret
: פרטי אימותoutput_variable
: היכן לאחסן את הטוקן המתקבל
- runs: מציין כיצד לבצע את הפעולה:
using: 'node20'
: משתמש בגרסת Node.js 20main: 'index.js'
: מפנה לקובץ JavaScript הראשי
כאשר משתמשים מתייחסים לפעולה זו בתהליכי העבודה שלהם, הם יספקו קלטים אלו על פי ההגדרת ממשק זה.
אופטימיזציה של הפעולה
כדי להפוך את הפעולה שלנו לניתנת לתחזוקה ויעילה יותר, נשתמש במכלול ncc
של Vercel כדי לאגד את כל התלויות לקובץ אחד. זה מסלק את הצורך להתחייב לתיקיית node_modules
:
אין להכליל את node_modules במאגר הפעולה שלך ב-GitHub מכמה סיבות:
- הקטלוג node_modules עשוי להיות מאוד גדול, מכיל את כל התלויות ואת התלויות המשניות שלהן, מה שיכול להגדיל את גודל המאגר ללא צורך
- מערכות הפעלה שונות וסביבות עשויות לטפל ב-node_modules בצורה שונה, מה שעשוי לגרום לבעיות תאימות
- שימוש במקמפיילר 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:
צעדים:
- שם: שלב דרישת קדם
שימוש: example/action-name@v1
עם:
קלטשם: ${{ 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:
צעדים:
- name: Step Name
uses: your-org/action-name@v1
with:
inputname: ‘Input Value’
anotherinput: ‘Another Value’
## Example Workflow Here's a complete example workflow utilizing this action:
name: Example Workflow
on: [push]
jobs:
example-job:
runs-on: ubuntu-latest
steps:
– name: Checkout Repository
uses: 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).
Key Points to Remember
When creating your own custom action:
- Always implement thorough error handling and logging
- Use the
@actions/core
package for proper GitHub Actions integration - Bundle dependencies with
ncc
to keep the repository clean - Document inputs and outputs clearly in your
action.yml
- Consider security implications and mask sensitive values using
core.setSecret()
This authentication action will be used by our next action that retrieves secrets. Let’s move on to creating that action.
Step 3: Creating the “Get Secret” Action
You’ve done the hard work up to this point. You now know how to create a custom Github action. If you’re following along, you now need to repeat those steps for the DVLS get secret entry action as follows:
The Action Structure
dvls-actions/ ├── get-secret-entry/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md
The index.js File
// 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'
Optimizing the Action
- Compile the index file.
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 של ה-webhook מתוך הסודות (
secrets.SLACK_WEBHOOK_URL
)
זרימת עבודה חדשה:
- מוסיפה שלב אימות באמצעות פעולה מותאמת אישית של DVLS
- שולפת את כתובת ה-URL של ה-webhook של Slack בצורה מאובטחת מ-Devolutions Server
- משתמשת במשתני סביבה במקום בסודות
- שומר על אותה פונקציונליות התראות אך עם אבטחה מוגברת
העבודה החדשה מוסיפה שני שלבים לפני ההתראה ב-Slack:
- אימות עם שרת Devolutions באמצעות הפעולה
dvls-login
- משיכת כתובת ה-URL של ה-webhook של 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 Actions מדגימה כיצד פעולות מותאמות אישית יכולות לגשר על הפער בין כלים שונים תוך שמירה על פרקטיקות אבטחה מיטביות. גישה זו יכולה להיות מותאמת לאינטגרציות ושימושים שונים בזרימות העבודה שלך ב-DevOps.
Source:
https://adamtheautomator.com/custom-github-actions-guide/