יצירת פעולות מותאמות אישית ב-GitHub: מדריך מלא לצוותי פיתוח תוכנה

האם מצאת עצמך מעתיק ומדביק אותו קוד במהלך עבודה במספר זרים שונים ב-GitHub? כאשר נדרש לבצע אותה משימה במאגרי קוד שונים או בתהליכי עבודה שונים, יצירת פעולת GitHub משותפת היא הדרך הנכונה לפתור את הבעיה. במדריך זה, למד כיצד לבנות פעולת JavaScript מותאמת אישית ב-GitHub Action מאפס שתוכל לשתף בכל הארגון שלך.

הבנת פעולות ותהליכי עבודה ב-GitHub

לפני שנכנסים ליצירת פעולה מותאמת, נקבע קצת הקשר. תהליך עבודה ב-GitHub הוא תהליך אוטומטי שניתן להגדיר במאגר הקוד שלך כדי לבנות, לבדוק, לארוז, לשחרר או להפיץ כל פרויקט ב-GitHub. תהליכי העבודה אלה מורכבים מפעולה אחת או יותר שיכולות לרוץ סידרתית או במקביל.

פעולות GitHub הן המשימות הפרטיות שמרכיבות תהליך עבודה. חשב עליהן כעל בלוקים בנייה שניתן להשתמש בהם שוב ושוב – הן מטפלות במשימות ספציפיות כמו בדיקת קוד, הרצת בדיקות או התקנה על שרת. GitHub מספקת שלושה סוגים של פעולות:

  • פעולות תאי Docker
  • פעולות JavaScript
  • פעולות מרוכבות

במדריך זה, נתמקד ביצירת פעולת JavaScript מאחר שהיא רצה ישירות על המכונה המבצעת ויכולה להריץ במהירות.

הבעיה: מתי ליצור פעולה מותאמת אישית

בואו נבדוק מתי ולמה תרצו ליצור פעולת GitHub מותאמת אישית דרך דוגמה מעשית. במהלך המדריך זה, נשתמש בתרחיש ספציפי – שילוב עם שרת Devolutions (DVLS) לניהול סודות – על מנת להדגים את התהליך, אך המושגים יכולים להיות רלוונטיים לכל מצב בו נדרשת פעולה משותפת ומשתמשת.

💡 שימו לב: אם יש לכם שרת Devolutions (DVLS) ורוצים לדלג לחלק השימוש, אתם יכולים למצוא את הגרסה המושלמת ב-מאגר פעולות GitHub של Devolutions.

דמיינו שאתם מנהלים מספר זרימות עבודה של GitHub שצריכות לתקשר עם שירות חיצוני – בדוגמה שלנו, שליפת סודות מ-DVLS. כל זרימת עבודה שזקוקה לפונקציה הזו דורשת את אותם צעדים בסיסיים:

  1. חיבור לשירות החיצוני
  2. אימות
  3. ביצוע פעולות מסוימות
  4. טיפול בתוצאות

בלי פעולה משותפת, תצטרכו לה duplicar את הקוד הזה בכל זרימת עבודה. זה לא רק לא יעיל – זה גם קשה יותר לתחזוקה ונוטה יותר לשגיאות.

למה ליצור פעולה משותפת?

יצירת פעולה משותפת של GitHub מציעה מספר יתרונות מרכזיים החלים על כל תרחיש אינטגרציה:

  • שימוש חוזר בקוד: כתבו את קוד האינטגרציה פעם אחת והשתמשו בו בכל הזרימות עבודה והמאגרות
  • תחזוקה: עדכנו את הפעולה במקום אחד כדי להוציא שינויים בכל מקום שבו היא נמצאת
  • סטנדרטיזציה: ודאו שכל הצוותים פועלים לפי אותו תהליך למשימות משותפות
  • שליטת גרסאות: עקבו אחרי שינויים בקוד האינטגרציה והחזירו אם צריך
  • צמצום מורכבות: הפכו את הזרימות לע simples על ידי הפשטת פרטי ההיישום

דרישות קדם

قبل שתתחיל את המדריך הזה, ודא שיש לך את הדברים הבאים:

  • מאגר GitHub עם זרימת עבודה קיימת
  • ידע בסיסי ב-Git, כולל שכפול מאגרים ויצירת סניפים
  • גישה כבעל ארגון כדי ליצור ולנהל מאגרים משותפים
  • הבנה בסיסית של JavaScript ו-Node.js

בדוגמת המקרה שלנו, ניצור פעולה שמשתלבת עם DVLS, אך תוכל להתאים את המושגים לכל שירות חיצוני או פונקציה מותאמת שאתה זקוק לה.

מה תיצור

בסוף המדריך הזה, תבין כיצד:

  1. ליצור מאגר GitHub ציבורי לפעולות משותפות
  2. לבנות מספר פעולות מקושרות (ניצור שתיים כדוגמאות):
    • אחת לטיפול באותנטיקציה
    • אחרת לביצוע פעולות ספציפיות
  3. ליצור זרימת עבודה שמשתמשת בפעולות המותאמות שלך

נציג את המושגים הללו על ידי בניית פעולות שמשתלבות עם 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, אנו צריכים ל:

  1. להכין את סביבת ה-DVLS
    • ליצור סודות תואמים ב-DVLS
    • לבדוק את נקודות ה-API של DVLS לאימות ולגישה לסוד
  2. ליצור את מאגר הפעולות המשותף
    • לבנות פעולה לאימות של DVLS (dvls-login)
    • לבנות פעולה לקבלת ערכי סוד (dvls-get-secret-entry)
    • להשתמש במהדר ncc של Vercel כדי לארוז את הפעולות ללא node_modules
  3. לשנות את זרימת העבודה
    • להחליף את ההפניות לסודות של 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 שלנו:

  1. אימות עם DVLS באמצעות פרטי היישום
  2. קבלת מידע על הארון באמצעות האסימון שחזר
  3. איתור מזהה הרשומה הספציפית של הסוד שלנו
  4. אחזור ערך הסוד האמיתי

הבנת הזרימה הזו חיונית מכיוון שעלינו ליישם את אותם שלבים בפעולת GitHub שלנו, רק על ידי שימוש ב-JavaScript במקום PowerShell.

כאשר אתה יוצר פעולת מותאמת אישית שלך, תעקוב אחר תהליך דומה:

  1. זיהוי נקודות הקצה של ה-API שצריך לתקשר איתן
  2. בדוק את תהליך האימות והבאת הנתונים
  3. תעד את הצעדים שתצטרך ליישם בפעולה שלך

שלב 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 הראשי שיטפל בלוגיקת האימות:

  1. צור 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. זה כולל את יצירת קבצי החבילה הנחוצים והתקנת התלות שהפעולה שלנו תצטרך כדי לפעול בצורה תקינה.

  1. צור 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"
        }
    }
    
  2. התקן את התלויות על ידי הרצת npm install.
    npm install
    

    לאחר התקנת התלויות, תראה ספריית node_modules חדשה שנוצרה בתיקיית הפרוייקט שלך. ספרייה זו מכילה את כל החבילות הנדרשות שהפעולה שלך צריכה כדי לרוץ.

    הערה: בזמן שנעביר package.json ו-package-lock.json לשליטת גירסאות, נכלול בסופו של דבר את ספריית node_modules על ידי שימוש ב-ncc כדי לארוז את התלויות שלנו.

  3. צור 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: היכן למצוא את שרת Devolutions
      • app_key ו-app_secret: פרטי אימות
      • output_variable: היכן לאחסן את הטוקן המתקבל
    • runs: מציין כיצד לבצע את הפעולה:
      • using: 'node20': משתמש בגרסת Node.js 20
      • main: 'index.js': מפנה לקובץ JavaScript הראשי

    כאשר משתמשים מתייחסים לפעולה זו בתהליכי העבודה שלהם, הם יספקו קלטים אלו על פי ההגדרת ממשק זה.

אופטימיזציה של הפעולה

כדי להפוך את הפעולה שלנו לניתנת לתחזוקה ויעילה יותר, נשתמש במכלול ncc של Vercel כדי לאגד את כל התלויות לקובץ אחד. זה מסלק את הצורך להתחייב לתיקיית node_modules:

אין להכליל את node_modules במאגר הפעולה שלך ב-GitHub מכמה סיבות:

  • הקטלוג node_modules עשוי להיות מאוד גדול, מכיל את כל התלויות ואת התלויות המשניות שלהן, מה שיכול להגדיל את גודל המאגר ללא צורך
  • מערכות הפעלה שונות וסביבות עשויות לטפל ב-node_modules בצורה שונה, מה שעשוי לגרום לבעיות תאימות
  • שימוש במקמפיילר ncc של ורצל כדי לאגד את כל התלויות לקובץ אחד הוא גישה טובה יותר מכיוון ש:
    • יוצר פעולה יעילה יותר שניתן לתחזק אותה
    • מבטל את הצורך להתחייב לקטלוג node_modules
  1. להתקין ncc:
    npm i -g @vercel/ncc
    
  2. לבנות את הגרסה המאוגדת:
    ncc build index.js --license licenses.txt
    
  3. לעדכן את action.yml כדי להפנות לקובץ המאוגד:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # עודכן לשימוש בגרסה המאוגדת
    
  4. לנקות:
    rm -rf node_modules  # הסרת קטלוג node_modules
    
  5. להתחייב לקבצים במאגר המשותף.
    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’
    another
    input: ‘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:

  1. Always implement thorough error handling and logging
  2. Use the @actions/core package for proper GitHub Actions integration
  3. Bundle dependencies with ncc to keep the repository clean
  4. Document inputs and outputs clearly in your action.yml
  5. 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

  1. Compile the index file.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. עדכון action.yml כדי להפנות לקובץ המאוחד:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # עודכן לשימוש בגרסה המאוחדת
    
  3. ניקוי:
    rm -rf node_modules  # הסרת תיקיית node_modules
    
  4. ביצוע מחויבות לקבצים במאגר המשותף.
    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:

  1. אימות עם שרת Devolutions באמצעות הפעולה dvls-login
  2. משיכת כתובת ה-URL של ה-webhook של Slack באמצעות הפעולה dvls-get-secret-entry
  3. שלב ההתראה הסופי ב-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/