创建自定义 GitHub Actions:DevOps 团队的完整指南

你有没有发现自己在多个 GitHub 工作流中复制粘贴相同的代码?当你需要在不同的代码库或工作流中执行相同的任务时,创建一个共享的 GitHub Action 是最佳选择。在本教程中,学习如何从头开始构建一个自定义的 JavaScript GitHub Action,以便在你的组织中共享。

理解 GitHub Actions 和工作流

在深入创建自定义 action 之前,让我们先建立一些背景知识。GitHub 工作流是你可以在代码库中设置的自动化过程,用于构建、测试、打包、发布或部署任何 GitHub 项目。这些工作流由一个或多个作业组成,这些作业可以顺序运行或并行运行。

GitHub Actions 是构成工作流的单个任务。可以将它们视为可重用的构建块 – 它们处理特定的任务,例如检出代码、运行测试或部署到服务器。GitHub 提供三种类型的 actions:

  • Docker 容器 actions
  • JavaScript actions
  • 复合 actions

在本教程中,我们将重点创建一个 JavaScript action,因为它直接在运行器机器上运行,执行速度较快。

问题:何时创建自定义 action

让我们通过一个实际的例子来探讨何时以及为何要创建自定义 GitHub Action。在本教程中,我们将使用一个特定场景——与 Devolutions Server (DVLS) 集成以进行秘密管理——来演示这个过程,但这些概念适用于任何需要创建共享、可重用 action 的情况。

💡 注意:如果您使用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 的 secret 中,但我们希望改为从我们的 DVLS 实例中检索。虽然这只是一个简单的示例,使用了一个 secret,但想象一下,在您的组织中有数十个工作流,每个工作流都使用多个 secret。在 DVLS 中集中管理这些 secret,而不是散落在 GitHub 中,将会更加高效。

实施计划

为了将这个工作流从使用 GitHub secret 转换为使用 DVLS,我们需要:

  1. 准备 DVLS 环境
    • 在 DVLS 中创建相应的 secrets
    • 测试 DVLS API 端点以进行认证和 secret 检索
  2. 创建共享操作存储库
    • 构建用于 DVLS 认证的操作(dvls-login
    • 构建用于检索 secret 值的操作(dvls-get-secret-entry
    • 使用 Vercel 的 ncc 编译器打包操作,不包括 node_modules
  3. 修改工作流
    • 用我们的自定义操作替换 GitHub secrets 的引用
    • 测试新的实现

每一步都建立在前一步的基础上,到最后,您将拥有一个可重复使用的解决方案,您的组织中的任何工作流都可以利用它。虽然我们以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操作,用于认证到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.jsonpackage-lock.json提交到版本控制,但我们最终将使用ncc来捆绑我们的依赖项,从而排除node_modules目录。

  3. 创建action.yml来定义操作的接口:
    name: 'Devolutions Server Login'
    description: '从Devolutions服务器进行身份验证并获取令牌'
    inputs:
      server_url:
        description: 'Devolutions服务器的URL'
        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_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:

步骤:

  • 名称:先决步骤
    使用:示例/操作名称@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:

名称:示例工作流
触发: [push]

作业:
示例作业:
运行环境: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 以指向捆绑的文件:
    运行:
      使用:'node20'
      主:'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 服务器安全地检索 Slack webhook URL
  • 使用环境变量而不是密钥
  • 保持相同的通知功能,但增强安全性

新的工作流在Slack通知之前添加了两个步骤:

  1. 使用dvls-login操作进行Devolutions服务器的身份验证
  2. 使用dvls-get-secret-entry操作检索Slack webhook URL
  3. 最后的Slack通知步骤保持类似,但使用从环境变量(env.SLACK_WEBHOOK_URL)检索的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 Actions可帮助您在多个存储库中标准化和保护工作流程。通过将身份验证和秘密检索等敏感操作移入专用操作,您可以:

  • 通过集中管理凭据管理来保持更好的安全实践
  • 减少在不同工作流程中的代码重复
  • 简化工作流程的维护和更新
  • 确保关键操作的一致实施

集成Devolutions服务器与GitHub Actions的示例演示了自定义操作如何在维护安全最佳实践的同时弥合不同工具之间的差距。这种方法可以应用于DevOps工作流程中的各种其他集成和用例。

Source:
https://adamtheautomator.com/custom-github-actions-guide/