커스텀 GitHub 액션 만들기: DevOps 팀을 위한 완벽 가이드

여러 개의 GitHub 워크플로에 동일한 코드를 복사하여 붙여넣기한 적이 있나요? 다른 저장소나 워크플로에서 동일한 작업을 수행해야 할 때는 공유 GitHub 액션을 만드는 것이 좋습니다. 본 자습서에서는 조직 내에서 공유할 수 있는 사용자 정의 JavaScript GitHub 액션을 처음부터 만드는 방법을 배워보겠습니다.

GitHub Actions와 워크플로 이해하기

사용자 정의 액션을 만드는 것에 들어가기 전에 일부 맥락을 확립해 봅시다. GitHub 워크플로는 GitHub에서 프로젝트를 빌드하거나 테스트하거나 패키지를 만들거나 릴리스하거나 배포할 수 있는 자동화된 프로세스입니다. 이러한 워크플로는 순차적으로 또는 병렬로 실행할 수 있는 하나 이상의 작업으로 구성됩니다.

GitHub Actions는 워크플로를 구성하는 개별 작업입니다. 이러한 작업은 재사용 가능한 구성 요소로, 코드 체크아웃, 테스트 실행 또는 서버로의 배포와 같은 특정 작업을 처리합니다. GitHub는 세 가지 유형의 작업을 제공합니다:

  • 도커 컨테이너 액션
  • JavaScript 액션
  • 컴포지트 액션

본 자습서에서는 러너 머신에서 직접 실행되어 빠르게 실행될 수 있는 JavaScript 액션을 만드는 데 초점을 맞출 것입니다.

문제: 언제 사용자 정의 액션을 만들어야 하는가

실제 예제를 통해 언제 그리고 왜 사용자 정의 GitHub 액션을 만들어야 하는지 살펴보겠습니다. 이 자습서를 통해 Devolutions Server(DVLS)와 통합하는 특정 시나리오를 사용하여 프로세스를 설명할 것이지만, 이러한 개념은 공유 가능한 재사용 가능한 작업을 만들어야 하는 모든 상황에 적용됩니다.

💡 참고: Devolutions Server (DVLS)를 사용하고 사용 부분으로 건너뛰고 싶다면, Devolutions Github Actions repo에서 완성된 버전을 찾을 수 있습니다.

여러 GitHub 워크플로를 관리한다고 상상해보십시오. 외부 서비스와 상호 작용해야 하는데, 예를 들어 DVLS에서 비밀을 가져오는 경우입니다. 이 기능이 필요한 각 워크플로는 동일한 기본 단계가 필요합니다:

  1. 외부 서비스에 연결
  2. 인증
  3. 특정 작업 수행
  4. 결과 처리

공유 액션 없이는이 코드를 모든 워크플로에 중복해서 작성해야 합니다. 이것은 효율적이지 않을 뿐만 아니라 유지 관리하기도 어렵고 오류가 발생할 가능성이 더 높아집니다.

왜 공유 액션을 만들어야 하는가?

공유 GitHub 액션을 만드는 것은 모든 통합 시나리오에 적용되는 주요 이점을 제공합니다:

  • 코드 재사용성: 통합 코드를 한 번 작성하고 여러 워크플로 및 저장소에서 사용
  • 유지 관리성: 사용되는 모든 곳에 변경 사항을 적용하기 위해 한 곳에서 액션 업데이트
  • 표준화: 모든 팀이 공통 작업에 대해 동일한 프로세스를 따르도록 보장
  • 버전 관리: 통합 코드의 변경 사항을 추적하고 필요한 경우 되돌릴 수 있음
  • 복잡성 감소: 구현 세부 정보를 추상화하여 워크플로를 단순화

전제 조건

이 튜토리얼을 시작하기에 앞서, 다음 사항이 준비되어 있는지 확인하세요:

  • 기존 워크플로우가 있는 GitHub 리포지토리
  • 리포지토리 복제 및 브랜치 생성 등 기본 Git 지식
  • 공유 리포지토리를 생성하고 관리할 수 있는 조직 소유자 접근 권한
  • JavaScript 및 Node.js에 대한 기본 이해

예시 시나리오로 DVLS와 통합되는 액션을 생성할 것이지만, 필요에 따라 어떤 외부 서비스나 사용자 정의 기능에도 개념을 적용할 수 있습니다.

당신이 만들게 될 것

이 튜토리얼이 끝날 무렵, 당신은 다음을 이해하게 될 것입니다:

  1. 공유 액션을 위한 공개 GitHub 리포지토리 생성
  2. 여러 개의 상호 연결된 액션 구축(예시로 두 개를 생성할 것입니다):
    • 인증을 처리하는 액션
    • 특정 작업을 수행하는 액션
  3. 사용자 정의 액션을 사용하는 워크플로우 생성

DVLS와 통합되는 액션을 구축하여 이러한 개념을 시연할 것이지만, 조직이 필요로 하는 어떤 목적을 위해서도 동일한 패턴을 적용하여 액션을 생성할 수 있습니다.

시작점: 기존 워크플로우

새 릴리스가 생성될 때 Slack 알림을 보내는 간단한 워크플로우를 살펴보겠습니다. 이 워크플로우는 현재 Slack 웹훅 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 참조에 유의하십시오. 이 웹훅 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 인스턴스에 이미 구성된 두 개의 비밀이 필요합니다:

  • 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 코드가 인증 토큰을 반환하기 위해 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 로그인'
    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_keyapp_secret: 인증 자격 증명
      • output_variable: 결과 토큰을 저장할 위치
    • runs: 액션을 실행하는 방법을 지정합니다:
      • using: 'node20': Node.js 버전 20을 사용합니다
      • main: 'index.js': 주요 JavaScript 파일을 가리킵니다

    사용자가 워크플로우에서 이 액션을 참조할 때, 이 인터페이스 정의에 따라 이러한 입력을 제공하게 됩니다.

작업 최적화

작업을 유지보수하고 효율적으로 만들기 위해 Vercel의 ncc 컴파일러를 사용하여 모든 종속성을 단일 파일로 번들링할 것입니다. 이렇게하면 node_modules 디렉터리를 커밋할 필요가 없어집니다:

GitHub 작업 저장소에 node_modules를 포함하는 것은 여러 가지 이유로 권장되지 않습니다:

  • 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 "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:

단계:

  • 이름: 사전 조건 단계
    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:

단계:

  • 이름: 단계 이름
    사용: your-org/action-name@v1
    with:
    입력이름: ‘입력 값’
    다른
    입력: ‘다른 값’
## 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. index 파일을 컴파일합니다.
    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 get secret entry 액션의 초기 커밋"
    git push
    

최종 결과

이 시점에서 두 개의 GitHub 레포지토리가 있어야 합니다:

  • GitHub Secrets를 사용하여 작업했던 워크플로우를 포함하는 레포지토리
  • 공유 레포지토리 (이름이 dvls-actions로 가정)은 다음과 같은 구조로 두 개의 액션을 포함합니다:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

사용자 정의 액션 사용

이러한 사용자 정의 액션을 설정한 후에는 원래의 호출 워크플로우에서 사용할 수 있습니다.

원래 워크플로우:

  • 슬랙 알림을 보내기 위한 단일 단계 사용
  • Secrets에서 웹훅 URL을 직접 참조 (secrets.SLACK_WEBHOOK_URL)

새로운 워크플로우:

  • 사용자 지정 DVLS 로그인 액션을 사용하여 인증 단계 추가
  • Devolutions Server에서 Slack 웹훅 URL을 안전하게 검색
  • Secrets 대신 환경 변수 사용
  • 알림 기능은 동일하게 유지되지만 보안이 강화되었습니다

새로운 워크플로우는 Slack 알림 전에 두 단계를 추가합니다:

  1. Devolutions Server와의 인증을 위한 dvls-login 작업
  2. 비밀 항목의 Slack 웹후크 URL 검색을 위한 dvls-get-secret-entry 작업
  3. 최종 Slack 알림 단계는 유사하게 유지되지만 환경 변수(env.SLACK_WEBHOOK_URL)에서 검색된 웹후크 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/