複数の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 からシークレットを取得するとします。この機能が必要な各ワークフローには、次の基本的な手順が必要です:
- 外部サービスに接続する
- 認証する
- 特定の操作を行う
- 結果を処理する
共有アクションがないと、このコードをすべてのワークフローで複製する必要があります。これは効率的でないだけでなく、メンテナンスが難しくなり、エラーが発生しやすくなります。
共有アクションを作成する理由は?
共有 GitHub アクションを作成すると、任意の統合シナリオに適用されるいくつかの主要な利点が得られます:
- コードの再利用: 統合コードを一度書いて複数のワークフローやリポジトリで使用する
- 保守性: 一か所でアクションを更新して、どこで使われているかに関係なく変更を展開する
- 標準化: 全チームが共通のタスクのために同じプロセスに従うことを保証する
- バージョン管理: 統合コードの変更を追跡し、必要に応じてロールバックする
- 複雑さの削減: 実装の詳細を抽象化することでワークフローを単純化する
前提条件
このチュートリアルを開始する前に、以下の準備が整っていることを確認してください:
- 既存のワークフローを持つGitHubリポジトリ
- リポジトリのクローンやブランチの作成などの基本的なGit知識
- 共有リポジトリを作成および管理するための組織所有者アクセス権限
- JavaScriptとNode.jsの基本的な理解
この例のシナリオでは、DVLSと統合するアクションを作成しますが、必要に応じて外部サービスやカスタム機能に概念を適応できます。
作成する内容
このチュートリアルの最後までに、以下の方法を理解することができます:
- 共有アクション用のパブリックGitHubリポジトリを作成
- 複数の連携したアクションを構築します(例として2つのアクションを作成します):
- 認証を処理するアクション
- 特定の操作を実行する別のアクション
- カスタムアクションを使用するワークフローを作成
これらの概念を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を使用するように変換するためには、以下の手順が必要です:
- DVLS環境の準備
- DVLSで対応するシークレットを作成
- 認証とシークレットの取得のためにDVLS APIエンドポイントをテスト
- 共有アクションリポジトリの作成
- DVLS認証用のアクションを構築(
dvls-login
) - シークレット値を取得するアクションを構築(
dvls-get-secret-entry
) - Vercelのnccコンパイラを使用して、アクションをnode_modulesなしでバンドル化
- DVLS認証用のアクションを構築(
- ワークフローの修正
- 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フローが明らかになります:
- アプリの認証情報を使用してDVLSに認証する
- 返されたトークンを使用してボールト情報を取得する
- 私たちのシークレットの特定のエントリーIDを見つける
- 実際のシークレット値を取得する
このフローを理解することは重要です。なぜなら、私たちはGitHub Actionで同じステップを実装する必要があり、PowerShellの代わりにJavaScriptを使用するからです。
自分自身のカスタムアクションを作成する際は、似たようなプロセスに従います:
- APIエンドポイントを特定する
- 認証およびデータ取得プロセスをテストする
- アクションで実装するための手順を文書化する
ステップ2:認証アクションの作成
APIフローが理解できたので、認証を処理する最初のカスタムアクションを作成しましょう。これを新しい共有リポジトリで構築します。
アクション構造のセットアップ
まず、リポジトリ内に以下のファイル構造を作成してください:
dvls-actions/ ├── login/ │ ├── index.js │ ├── action.yml │ ├── package.json │ └── README.md
このファイル構造は、モジュール化されて保守可能なGitHubアクションを作成するために整理されています:
- login/ – 関連するファイルを一緒に保持する認証アクションのための専用ディレクトリ
- index.js – 認証ロジックとAPIのやり取りを含むメインアクションコード
- action.yml – 必要な入力やアクションの実行方法を定義するアクションのインターフェース
- package.json – 依存関係やプロジェクトのメタデータを管理する
- README.md – アクションのユーザー向けのドキュメント
この構造は、GitHub Actionsのベストプラクティスに従い、コードを整理し、アクションを維持および更新しやすくしています。
アクションコードの作成
まず、アクションコードを作成する必要があります。これには、認証ロジックを処理するメインJavaScriptファイルを作成する必要があります:
index.js
を作成します – ここにメインアクションロジックが配置されます:
// Required dependencies // @actions/core - GitHub Actions toolkit for input/output operations const core = require('@actions/core'); // axios - HTTP client for making API requests const axios = require('axios'); // https - Node.js HTTPS module for SSL/TLS support const https = require('https'); // Create an axios instance with SSL verification disabled // This is useful when dealing with self-signed certificates const axiosInstance = axios.create({ httpsAgent: new https.Agent({ rejectUnauthorized: false }) }); /** * Authenticates with the Devolutions Server and retrieves an auth token * @param {string} serverUrl - The base URL of the Devolutions Server * @param {string} appKey - Application key for authentication * @param {string} appSecret - Application secret for authentication * @returns {Promise<string>} The authentication token */ async function getAuthToken(serverUrl, appKey, appSecret) { core.info(`Attempting to get auth token from ${serverUrl}/api/v1/login`); const response = await axiosInstance.post(`${serverUrl}/api/v1/login`, { appKey: appKey, appSecret: appSecret }); core.info('Successfully obtained auth token'); return response.data.tokenId; } /** * Wrapper function for making HTTP requests with detailed error handling * @param {string} description - Description of the request for logging * @param {Function} requestFn - Async function that performs the actual request * @returns {Promise<any>} The result of the request * @throws {Error} Enhanced error with detailed debugging information */ async function makeRequest(description, requestFn) { try { core.info(`Starting request: ${description}`); const result = await requestFn(); core.info(`Successfully completed request: ${description}`); return result; } catch (error) { // Detailed error logging for debugging purposes core.error('=== Error Details ==='); core.error(`Error Message: ${error.message}`); core.error(` core.error(`Status Text: ${error.response?.statusText}`); // Log response data if available if (error.response?.data) { core.error('Response Data:'); core.error(JSON.stringify(error.response.data, null, 2)); } // Log request configuration details if (error.config) { core.error('Request Details:'); core.error(`URL: ${error.config.url}`); core.error(`Method: ${error.config.method}`); core.error('Request Data:'); core.error(JSON.stringify(error.config.data, null, 2)); } core.error('=== End Error Details ==='); // Throw enhanced error with API message if available const apiMessage = error.response?.data?.message; throw new Error(`${description} failed: ${apiMessage || error.message} ( } } /** * Main execution function for the GitHub Action * This action authenticates with a Devolutions Server and exports the token * for use in subsequent steps */ async function run() { try { core.info('Starting Devolutions Server Login action'); // Get input parameters from the workflow const serverUrl = core.getInput('server_url'); const appKey = core.getInput('app_key'); const appSecret = core.getInput('app_secret'); const outputVariable = core.getInput('output_variable'); core.info(`Server URL: ${serverUrl}`); core.info('Attempting authentication...'); // Authenticate and get token const token = await makeRequest('Authentication', () => getAuthToken(serverUrl, appKey, appSecret) ); // Mask the token in logs for security core.setSecret(token); // Make token available as environment variable core.exportVariable(outputVariable, token); // Set token as output for other steps core.setOutput('token', token); core.info('Action completed successfully'); } catch (error) { // Handle any errors that occur during execution core.error(`Action failed: ${error.message}`); core.setFailed(error.message); } } // Execute the action run();
コードは、入力、出力、ログを処理するためにGitHubのツールキットから@actions/core
パッケージを使用しています。また、デバッグを容易にするために、堅牢なエラーハンドリングとロギングも実装しました。
ここでのJavaScriptコードの詳細をすべて理解しようとする必要はありません!重要なポイントは、このGitHub Actionコードが主に1つのことを行う必要があるということです:core.setOutput()
を使用して認証トークンを返すことです。
このJavaScriptを自分で書くことに不安がある場合は、ChatGPTのようなツールを使用してコードを生成する手助けを受けることができます。最も重要なのは、アクションが必要とすることを理解することです:
- 入力値を取得する(サーバーURLや資格情報など)
- 認証リクエストを行う
core.setOutput()
を使用してトークンを返す
NodeJSパッケージの作成
コードの構造と機能を理解したので、Node.jsパッケージの設定を行いましょう。これには、アクションが正しく機能するために必要なパッケージファイルを作成し、依存関係をインストールすることが含まれます。
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" } }
- npm installを実行して依存関係をインストールします。
npm install
依存関係をインストールした後、プロジェクトフォルダに新しい
node_modules
ディレクトリが作成されるはずです。このディレクトリには、アクションの実行に必要なすべてのパッケージが含まれています。注意:
package.json
とpackage-lock.json
をバージョン管理にコミットしますが、ncc
を使用して依存関係をバンドルすることで、最終的にnode_modules
ディレクトリを除外します。 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ディレクトリをコミットする必要がなくなる
ncc
をインストールする:npm i -g @vercel/ncc
- バンドルされたバージョンをビルドする:
ncc build index.js --license licenses.txt
action.yml
を更新してバンドルされたファイルを指定する:runs: using: 'node20' main: 'dist/index.js' # バンドルされたバージョンを使用するように更新
- クリーンアップ:
rm -rf node_modules # node_modulesディレクトリを削除
- ファイルを共有リポジトリにコミットする。
git add . git commit -m "DVLSログインアクションの初回コミット" git push
READMEの作成
誰もがドキュメントを愛していますよね?いいえ?私もそうではありませんが、あなたが使用するためのREADMEテンプレートを作成しました。これを記入してアクションに含めてください。
# GitHub Action Template This template provides a standardized structure for documenting any GitHub Action. Replace the placeholders with details specific to your action. --- # Action Name A brief description of what this GitHub Action does. ## Prerequisites Outline any setup or configuration required before using the action. For example:
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).
覚えておくべき重要なポイント
独自のカスタムアクションを作成する際:
- 常に徹底したエラーハンドリングとログ記録を実装すること
- 適切なGitHubアクション統合のために
@actions/core
パッケージを使用すること ncc
を使用して依存関係をバンドルし、リポジトリを整理することaction.yml
で入力と出力を明確に文書化すること- セキュリティ上の考慮事項を検討し、
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'
アクションの最適化
- インデックスファイルをコンパイルする。
npm i -g @vercel/ncc ncc build index.js --license licenses.txt
action.yml
をバンドルされたファイルにポイントするように更新します:runs: using: 'node20' main: 'dist/index.js' # バンドルバージョンを使用するように更新
- クリーンアップ:
rm -rf node_modules # node_modulesディレクトリを削除
- ファイルを共有リポジトリにコミットします。
git add . git commit -m "DVLSシークレットエントリーアクションの初回コミット" git push
最終結果
この時点で、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つのステップを追加する新しいワークフロー:
dvls-login
アクションを使用したDevolutions Serverによる認証dvls-get-secret-entry
アクションを使用したSlack webhook URLの取得- 最終の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/