カスタムGitHub Actionsの作成:DevOpsチームのための完全ガイド

複数のGitHubワークフローで同じコードをコピー&ペーストしたことがありますか?異なるリポジトリやワークフローで同じ作業を行う必要がある場合、共有GitHubアクションを作成するのが最適です。このチュートリアルでは、組織内で共有できるカスタムJavaScript GitHubアクションをゼロから構築する方法を学びます。

GitHubアクションとワークフローの理解

カスタムアクションの作成に入る前に、いくつかの背景を確認しましょう。GitHubワークフローは、リポジトリ内で設定できる自動化プロセスで、プロジェクトをビルド、テスト、パッケージ、リリース、またはデプロイできます。これらのワークフローは、順次または並行して実行できる1つ以上のジョブで構成されています。

GitHubアクションは、ワークフローを構成する個々のタスクです。これらは再利用可能なビルディングブロックとして考えてください – コードのチェックアウト、テストの実行、サーバーへのデプロイなど、特定のタスクを処理します。GitHubは3種類のアクションを提供しています:

  • 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. 複数の連携したアクションを構築します(例として2つのアクションを作成します):
    • 認証を処理するアクション
    • 特定の操作を実行する別のアクション
  3. カスタムアクションを使用するワークフローを作成

これらの概念をDVLSと統合するアクションの構築で示しますが、組織が必要とする目的に合わせて任意のアクションを作成するために同じパターンを適用できます。

出発点:既存のワークフロー

新しいリリースが作成されたときにSlack通知を送信する簡単なワークフローを検討しましょう。このワークフローでは、現在、SlackのWebhook URLを保存するためにGitHubシークレットを使用しています。

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インスタンスから取得したいと考えています。これは単一のシークレットを使用した単純な例ですが、組織全体で複数のワークフローを持ち、それぞれが複数のシークレットを使用する状況を想像してください。これらのシークレットをGitHubに散在させるのではなく、DVLSで中央管理することがより効率的でしょう。

実装計画

このワークフローを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 Actionを作成する前に、外部サービスとのインタラクション方法を理解する必要があります。私たちのDVLSの例では、DVLSインスタンスに既に設定された2つのシークレットが必要です:

  • 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 Actionで実装する必要があるAPIフローが明らかになります:

  1. アプリの認証情報を使用してDVLSに認証する
  2. 返されたトークンを使用してボールト情報を取得する
  3. 私たちのシークレットの特定のエントリーIDを見つける
  4. 実際のシークレット値を取得する

このフローを理解することは重要です。なぜなら、私たちはGitHub Actionで同じステップを実装する必要があり、PowerShellの代わりにJavaScriptを使用するからです。

自分自身のカスタムアクションを作成する際は、似たようなプロセスに従います:

  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 Actionsのベストプラクティスに従い、コードを整理し、アクションを維持および更新しやすくしています。

アクションコードの作成

まず、アクションコードを作成する必要があります。これには、認証ロジックを処理するメイン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コードが主に1つのことを行う必要があるということです:core.setOutput()を使用して認証トークンを返すことです。

このJavaScriptを自分で書くことに不安がある場合は、ChatGPTのようなツールを使用してコードを生成する手助けを受けることができます。最も重要なのは、アクションが必要とすることを理解することです:

  • 入力値を取得する(サーバーURLや資格情報など)
  • 認証リクエストを行う
  • core.setOutput()を使用してトークンを返す

NodeJSパッケージの作成

コードの構造と機能を理解したので、Node.jsパッケージの設定を行いましょう。これには、アクションが正しく機能するために必要なパッケージファイルを作成し、依存関係をインストールすることが含まれます。

  1. package.jsonを作成して、依存関係やその他のアクションのメタデータを定義します。
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "Devolutions Serverに認証するGitHub Action",
        "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 Serverからトークンを認証して取得する'
    inputs:
      server_url:
        description: 'Devolutions Serverの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 Serverの場所
      • app_keyおよびapp_secret: 認証情報
      • output_variable: 結果のトークンを格納する場所
    • runs: アクションを実行する方法を指定します:
      • using: 'node20': Node.jsバージョン20を使用します
      • main: 'index.js': メインのJavaScriptファイルを指します

    ユーザーがワークフロー内でこのアクションを参照する際には、このインターフェース定義に従ってこれらの入力を提供します。

アクションの最適化

アクションをより維持管理しやすく、効率的にするために、Vercelのnccコンパイラを使用してすべての依存関係を単一のファイルにバンドルします。これにより、node_modulesディレクトリをコミットする必要がなくなります:

GitHub Actionリポジトリにnode_modulesを含めることは、いくつかの理由から推奨されません:

  • node_modulesディレクトリは非常に大きく、すべての依存関係とそのサブ依存関係を含んでおり、リポジトリのサイズを不必要に膨らませる可能性があります
  • 異なるオペレーティングシステムや環境は、node_modulesを異なる方法で処理するため、互換性の問題を引き起こす可能性があります
  • Vercelのnccコンパイラを使用してすべての依存関係を1つのファイルにバンドルすることは、より良いアプローチです。なぜなら、それは:
    • より効率的でメンテナンスしやすいアクションを作成する
    • 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:

steps:

  • name: 事前ステップ
    uses: example/action-name@v1
    with:
    inputname: ${{ secrets.INPUTSECRET }}
## Inputs

| Input Name       | Description                                    | Required | Default        |
|-------------------|------------------------------------------------|----------|----------------|
| `input_name`     | Description of the input parameter             | Yes/No   | Default Value  |
| `another_input`  | Description of another input parameter         | Yes/No   | Default Value  |

## Outputs

| Output Name      | Description                                    |
|-------------------|------------------------------------------------|
| `output_name`    | Description of the output parameter            |
| `another_output` | Description of another output parameter        |

## Usage

Provide an example of how to use this action in a workflow:

steps:

  • 名前:ステップ名
    使用:your-org/action-name@v1
    with:
    input名前:’入力値’
    別の
    input:’別の値’
## Example Workflow

Here's a complete example workflow utilizing this action:

名前:例のワークフロー
on: [push]

ジョブ:
example-job:
runs-on: ubuntu-latest
steps:
– 名前:リポジトリをチェックアウト
使用: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. 適切なGitHubアクション統合のために@actions/coreパッケージを使用すること
  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
    

最終結果

この時点で、2つのGitHubリポジトリがあるはずです:

  • GitHubシークレットを使用していたワークフローを含むリポジトリ
  • 2つのアクションを含む共有リポジトリ(名前がdvls-actionsであると仮定)で、構造は以下のようになります:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

カスタムアクションの使用

これらのカスタムアクションを設定したら、元の呼び出しワークフローで使用できます。

元のワークフロー:

  • 単一ステップを使用してSlack通知を送信
  • シークレットからウェブフックURLを直接参照(secrets.SLACK_WEBHOOK_URL

新しいワークフロー:

  • カスタムDVLSログインアクションを使用した認証ステップを追加
  • Devolutions ServerからSlackウェブフックURLを安全に取得
  • シークレットの代わりに環境変数を使用
  • 同じ通知機能を維持しつつセキュリティを強化

Slack通知の前に2つのステップを追加する新しいワークフロー:

  1. dvls-loginアクションを使用したDevolutions Serverによる認証
  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 ServerをGitHub Actionsと統合する例は、カスタムアクションが異なるツール間のギャップを埋めながらセキュリティベストプラクティスを維持する方法を示しています。このアプローチは、DevOpsワークフローのさまざまな他の統合やユースケースに適応できます。

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