建立自訂 GitHub 行動:DevOps 團隊的完整指南

曾經發現自己在多個GitHub工作流程中複製和粘貼相同的代碼嗎?當您需要在不同存儲庫或工作流程中執行相同任務時,創建共享的GitHub操作是正確的方法。在本教程中,學習如何從頭開始構建一個自定義的JavaScript GitHub操作,以便您可以在組織中共享。

理解GitHub操作和工作流程

在深入創建自定義操作之前,讓我們建立一些背景。GitHub工作流程是您可以在存儲庫中設置的自動化流程,用於構建、測試、打包、發布或部署GitHub上的任何項目。這些工作流程由一個或多個可以依次運行或並行運行的作業組成。

GitHub操作是構成工作流程的個別任務。將它們視為可重用的構建塊 – 它們處理特定任務,如檢查代碼、運行測試或部署到服務器。GitHub提供三種類型的操作:

  • Docker容器操作
  • JavaScript操作
  • 合成操作

在本教程中,我們將專注於創建JavaScript操作,因為它直接在運行機器上運行並且可以快速執行。

問題:何時創建自定義操作

讓我們通過一個實際示例探索何時以及為什麼您會想要創建自定義GitHub操作。在整個本教程中,我們將使用特定情境 – 與Devolutions Server (DVLS) 集成進行秘密管理 – 來演示這個過程,但這些概念適用於任何需要創建共享、可重用操作的情況。

💡 注意:如果您使用Devolutions Server (DVLS)並希望跳過使用部分,您可以在Devolutions Github Actions存儲庫中找到已完成的版本。

想象一下,您正在管理多個需要與外部服務交互的GitHub工作流程 – 在我們的示例中,從DVLS檢索密碼。需要此功能的每個工作流程都需要相同的基本步驟:

  1. 連接到外部服務
  2. 驗證
  3. 執行特定操作
  4. 處理結果

如果沒有共享操作,您將需要在每個工作流程中重複此代碼。這不僅效率低下 – 也更難以維護且更容易出錯。

為什麼創建共享操作?

創建共享的GitHub操作提供了幾個關鍵好處,適用於任何集成方案:

  • 代碼重用:編寫集成代碼一次,然後在多個工作流程和存儲庫中使用
  • 可維護性:在一個地方更新操作以推出到所有使用它的地方的更改
  • 標準化:確保所有團隊遵循共同任務的相同流程
  • 版本控制:跟踪集成代碼的更改並在需要時回滾
  • 減少複雜性:通過抽象實施細節來簡化工作流程

先決條件

在開始這個教程之前,請確保您已經準備好以下事項:

  • 一個帶有現有工作流程的 GitHub 存儲庫
  • 基本的 Git 知識,包括克隆存儲庫和創建分支
  • 組織擁有者訪問權限,可以創建和管理共享存儲庫
  • 基本理解 JavaScript 和 Node.js

在我們的示例情境中,我們將創建一個與 DVLS 集成的操作,但您可以將這些概念適應到您需要的任何外部服務或自定義功能。

您將創建什麼

通過本教程,您將了解如何:

  1. 為共享操作創建一個公共 GitHub 存儲庫
  2. 構建多個相互關聯的操作(我們將創建兩個示例):
    • 一個用於處理身份驗證
    • 另一個用於執行特定操作
  3. 創建使用您自定義操作的工作流程

我們將通過構建與 DVLS 集成的操作來演示這些概念,但您可以應用相同的模式來為組織需要的任何目的創建操作。

起點:現有的工作流程

讓我們來檢查一個簡單的工作流程,當創建新版本時發送 Slack 通知。這個工作流程目前使用 GitHub 密鑰來存儲 Slack webhook URL:

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 URL被儲存為GitHub的秘密,但我們想要改為從我們的DVLS實例檢索它。儘管這只是一個簡單的示例,僅使用一個秘密,但想像一下在您的組織中有數十個工作流,每個工作流都使用多個秘密。將這些秘密集中管理在DVLS中,而不是散佈在GitHub上,將會更加高效。

實施計劃

為了將此工作流從使用GitHub秘密轉換為DVLS,我們需要:

  1. 準備DVLS環境
    • 在DVLS中創建相應的秘密
    • 測試DVLS API端點以進行驗證和秘密檢索
  2. 創建共享操作存儲庫
    • 為DVLS驗證創建一個操作(dvls-login
    • 為檢索秘密值創建一個操作(dvls-get-secret-entry
    • 使用Vercel的ncc編譯器將操作打包而不包含node_modules
  3. 修改工作流
    • 將GitHub秘密引用替換為我們的自定義操作
    • 測試新的實施

每個步驟都建立在前一個步驟之上,最終,您將擁有一個可重複使用的解決方案,您組織中的任何工作流程都可以利用。雖然我們以DVLS為例,但您可以將同樣的模式適應到您的工作流程需要與任何外部服務互動的情況。

步驟1:探索外部API

在創建GitHub操作之前,您需要了解如何與外部服務進行交互。對於我們的DVLS示例,我們需要在DVLS實例中預先配置兩個秘密:

  • DVLS_APP_KEY – 用於身份驗證的應用程式金鑰
  • DVLS_APP_SECRET – 用於身份驗證的應用程式密鑰

測試API流程

讓我們使用PowerShell來探索DVLS API並了解我們需要在操作中實現的流程。在創建任何自定義操作時,這個探索階段至關重要 – 您需要在實現它們之前了解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

這個探索揭示了我們需要在GitHub操作中實現的API流程:

  1. 使用應用程式憑證與DVLS進行身份驗證
  2. 使用返回的令牌獲取金庫信息
  3. 查找我們秘密的特定條目ID
  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();

該代碼使用了 GitHub 工具包中的 @actions/core 套件來處理輸入、輸出和日誌記錄。我們還實現了強大的錯誤處理和日誌記錄,以便更輕鬆地進行調試。

不用太擔心在這裡理解所有 JavaScript 代碼的細節!關鍵是這個 GitHub Action 代碼只需要做一件主要的事情:使用 core.setOutput() 來返回身份驗證令牌。

如果你不習慣自己編寫這個 JavaScript 代碼,你可以使用像 ChatGPT 這樣的工具來幫助生成代碼。最重要的部分是了解這個動作需要:

  • 獲取輸入值(如服務器 URL 和憑證)
  • 進行身份驗證請求
  • 使用 core.setOutput() 返回令牌

創建 NodeJS 封裝

現在我們了解了動作的代碼結構和功能,讓我們設置 Node.js 封裝配置。這涉及創建必要的封裝文件並安裝我們動作所需的依賴項。

  1. 建立package.json來定義我們的依賴項和其他操作元數據。
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "GitHub Action 用於驗證到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"
        }
    }
    
  2. 通過運行npm install安裝依賴項。
    npm install
    

    安裝依賴項後,您應該在項目文件夾中看到一個新的node_modules目錄。該目錄包含您的操作運行所需的所有必要包。

    注意:雖然我們將package.jsonpackage-lock.json提交到版本控制,但最終我們將使用ncc來捆綁我們的依賴項,從而排除node_modules目錄。

  3. 創建action.yml來定義動作的接口:
    name: 'Devolutions Server Login'
    description: 'Authenticate and get a token from Devolutions Server'
    inputs:
      server_url:
        description: 'URL of the Devolutions Server'
        required: true
      app_key:
        description: 'Application key for authentication'
        required: true
      app_secret:
        description: 'Application secret for authentication'
        required: true
      output_variable:
        description: 'Name of the environment variable to store the retrieved token'
        required: false
        default: 'DVLS_TOKEN'
    runs:
      using: 'node20'
      main: 'index.js'
    

    action.yml文件至關重要,因為它定義了您的操作在 GitHub Actions 工作流中的工作方式。讓我們分解其關鍵組件:

    • name和description:這些提供了有關您的操作功能的基本信息
    • inputs:定義用戶可以傳遞給您的操作的參數:
      • server_url:Devolutions Server 的位置
      • app_keyapp_secret:身份驗證凭證
      • output_variable:存儲結果令牌的位置
    • runs:指定如何執行操作:
      • using: 'node20':使用 Node.js 版本20
      • main: 'index.js':指向主 JavaScript 文件

    當用戶在其工作流程中引用此操作時,他們將根據此接口定義提供這些輸入。

優化操作

為了使我們的操作更易於維護和高效,我們將使用 Vercel 的 ncc 編譯器將所有依賴項捆綁成單個文件。這樣就無需提交 node_modules 目錄:

將 node_modules 包含在您的 GitHub 操作存儲庫中是不建議的,原因有幾個:

  • node_modules目錄可能非常龐大,包含所有的依賴項及其子依賴項,這將使存儲庫的大小不必要地膨脹
  • 不同的操作系統和環境可能以不同方式處理node_modules,可能會導致兼容性問題
  • 使用Vercel的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 "Initial commit of DVLS login action"
    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:

步驟:

  • 名稱: 步驟名稱
    使用: 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).

要牢記的重點

在創建自定義操作時:

  1. 始終實施徹底的錯誤處理和記錄
  2. 使用 @actions/core 套件進行適當的 GitHub Actions 整合
  3. 使用 ncc 打包依賴項,以保持存儲庫清潔
  4. 清楚地在您的 action.yml 文件中記錄輸入和輸出
  5. 考慮安全性問題,使用 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'

優化操作

  1. 編譯索引文件。
    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 通知
  • 直接從秘密中引用 Webhook URL(secrets.SLACK_WEBHOOK_URL

新工作流程:

  • 添加使用自定義 DVLS 登錄操作的身份驗證步驟
  • 從 Devolutions Server 安全檢索 Slack Webhook URL
  • 使用環境變量代替秘密。
  • 保持相同的通知功能,但增強了安全性

新的工作流在Slack通知之前添加了兩個步驟:

  1. 使用dvls-login操作與Devolutions服務器進行身份驗證
  2. 使用dvls-get-secret-entry操作檢索Slack webhook URL
  3. 最終的Slack通知步驟保持相似,但使用從環境變量中檢索的webhook 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/