Creación de acciones personalizadas de GitHub: Una guía completa para equipos de DevOps

¿Alguna vez te has encontrado copiando y pegando el mismo código en múltiples flujos de trabajo de GitHub? Cuando necesitas realizar la misma tarea en diferentes repositorios o flujos de trabajo, crear una Acción de GitHub compartida es la mejor opción. En este tutorial, aprende cómo construir una Acción de JavaScript personalizada desde cero que puedas compartir en toda tu organización.

Comprendiendo las Acciones y Flujos de trabajo de GitHub

Antes de adentrarnos en la creación de una acción personalizada, establezcamos un poco de contexto. Un flujo de trabajo de GitHub es un proceso automatizado que puedes configurar en tu repositorio para construir, probar, empaquetar, lanzar o implementar cualquier proyecto en GitHub. Estos flujos de trabajo están compuestos por uno o más trabajos que pueden ejecutarse de forma secuencial o en paralelo.

Las Acciones de GitHub son las tareas individuales que componen un flujo de trabajo. Piensa en ellas como bloques de construcción reutilizables: manejan tareas específicas como verificar el código, ejecutar pruebas o implementar en un servidor. GitHub proporciona tres tipos de acciones:

  • Acciones de contenedor Docker
  • Acciones de JavaScript
  • Acciones compuestas

Para este tutorial, nos enfocaremos en crear una acción de JavaScript ya que se ejecuta directamente en la máquina del corredor y puede ejecutarse rápidamente.

El Problema: Cuándo crear una Acción Personalizada

Examinemos cuándo y por qué querrías crear una Acción de GitHub personalizada a través de un ejemplo práctico. A lo largo de este tutorial, utilizaremos un escenario específico: integrar con el Servidor de Devolutions (DVLS) para la gestión de secretos, para demostrar el proceso, pero los conceptos se aplican a cualquier situación donde necesites crear una acción compartida y reutilizable.

💡 Nota: Si tienes Devolutions Server (DVLS) y deseas ir directamente a la parte de uso, puedes encontrar la versión completa en el repositorio de Devolutions Github Actions.

Imagina que estás gestionando múltiples flujos de trabajo de GitHub que necesitan interactuar con un servicio externo, en nuestro ejemplo, recuperando secretos de DVLS. Cada flujo de trabajo que necesita esta funcionalidad requiere los mismos pasos básicos:

  1. Conectar con el servicio externo
  2. Autenticar
  3. Realizar operaciones específicas
  4. Manejar los resultados

Sin una acción compartida, tendrías que duplicar este código en cada flujo de trabajo. Eso no solo es ineficiente, también es más difícil de mantener y más propenso a errores.

¿Por qué crear una Acción Compartida?

Crear una Acción Compartida en GitHub ofrece varios beneficios clave que se aplican a cualquier escenario de integración:

  • Reutilización de código: Escribe el código de integración una vez y úsalo en múltiples flujos de trabajo y repositorios
  • Mantenibilidad: Actualiza la acción en un solo lugar para implementar cambios en todos los lugares donde se use
  • Estandarización: Asegura que todos los equipos sigan el mismo proceso para tareas comunes
  • Control de versiones: Realiza un seguimiento de los cambios en el código de integración y retrocede si es necesario
  • Reducción de la complejidad: Simplifica los flujos de trabajo abstrayendo los detalles de implementación

Requisitos

Antes de comenzar este tutorial, asegúrate de tener lo siguiente en su lugar:

  • Un repositorio de GitHub con un flujo de trabajo existente
  • Conocimientos básicos de Git, incluyendo la clonación de repositorios y la creación de ramas
  • Acceso de propietario de la organización para crear y gestionar repositorios compartidos
  • Conocimiento básico de JavaScript y Node.js

Para nuestro escenario de ejemplo, crearemos una acción que se integra con DVLS, pero puedes adaptar los conceptos a cualquier servicio externo o funcionalidad personalizada que necesites.

Lo que crearás

Al final de este tutorial, entenderás cómo:

  1. Crear un repositorio público de GitHub para acciones compartidas
  2. Construir múltiples acciones interconectadas (crearemos dos como ejemplos):
    • Una para manejar la autenticación
    • Otra para realizar operaciones específicas
  3. Crear un flujo de trabajo que utilice tus acciones personalizadas

Demostraremos estos conceptos construyendo acciones que se integran con DVLS, pero puedes aplicar los mismos patrones para crear acciones para cualquier propósito que necesite tu organización.

Punto de partida: El flujo de trabajo existente

Examinemos un flujo de trabajo simple que envía una notificación de Slack cuando se crea una nueva versión. Este flujo de trabajo actualmente utiliza secretos de GitHub para almacenar la URL del webhook de Slack:

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:"
          }'

Ten en cuenta la referencia secrets.SLACK_WEBHOOK_URL. Esta URL del webhook actualmente está almacenada como un secreto de GitHub, pero queremos recuperarla desde nuestra instancia de DVLS en su lugar. Aunque este es un ejemplo simple que utiliza solo un secreto, imagina tener docenas de flujos de trabajo en toda tu organización, cada uno utilizando múltiples secretos. Gestionar estos secretos de forma centralizada en DVLS en lugar de dispersos por GitHub sería mucho más eficiente.

Plan de Implementación

Para convertir este flujo de trabajo de usar secretos de GitHub a DVLS, necesitamos:

  1. Preparar el Entorno de DVLS
    • Crear secretos correspondientes en DVLS
    • Probar los puntos finales de la API de DVLS para autenticación y recuperación de secretos
  2. Crear el Repositorio de Acciones Compartidas
    • Crear una acción para la autenticación de DVLS (dvls-login)
    • Crear una acción para recuperar los valores de secretos (dvls-get-secret-entry)
    • Usar el compilador ncc de Vercel para empaquetar las acciones sin node_modules
  3. Modificar el Flujo de Trabajo
    • Reemplazar las referencias a secretos de GitHub con nuestras acciones personalizadas
    • Probar la nueva implementación

Cada paso se basa en el anterior, y al final, tendrás una solución reutilizable que cualquier flujo de trabajo en tu organización puede aprovechar. Si bien estamos utilizando DVLS como ejemplo, puedes adaptar este mismo patrón para cualquier servicio externo con el que tus flujos de trabajo necesiten interactuar.

Paso 1: Explorando la API Externa

Antes de crear una Acción de GitHub, necesitas entender cómo interactuar con tu servicio externo. Para nuestro ejemplo de DVLS, necesitamos dos secretos ya configurados en la instancia de DVLS:

  • DVLS_APP_KEY – La clave de la aplicación para la autenticación
  • DVLS_APP_SECRET – El secreto de la aplicación para la autenticación

Probando el Flujo de la API

Usaremos PowerShell para explorar la API de DVLS y entender el flujo que necesitaremos implementar en nuestra acción. Esta fase de exploración es crucial al crear cualquier acción personalizada: necesitas entender los requisitos de la API antes de implementarlos.

$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

Esta exploración revela el flujo de la API que necesitaremos implementar en nuestra Acción de GitHub:

  1. Autenticar con DVLS usando credenciales de la aplicación
  2. Obtener la información del almacén usando el token devuelto
  3. Localizar el ID de entrada específico para nuestro secreto
  4. Recuperar el valor real del secreto

Entender este flujo es crucial porque necesitaremos implementar los mismos pasos en nuestra Acción de GitHub, simplemente usando JavaScript en lugar de PowerShell.

Cuando crees tu propia acción personalizada, seguirás un proceso similar:

  1. Identifica los puntos finales de la API con los que necesitas interactuar
  2. Prueba el proceso de autenticación y recuperación de datos
  3. Documenta los pasos que necesitarás implementar en tu acción

Paso 2: Creando la Acción de Autenticación

Ahora que entendemos el flujo de la API, vamos a crear nuestra primera acción personalizada para manejar la autenticación. Lo construiremos en un nuevo repositorio compartido.

Configurando la Estructura de la Acción

Primero, crea la siguiente estructura de archivos en tu repositorio:

dvls-actions/
├── login/
│   ├── index.js
│   ├── action.yml
│   ├── package.json
│   └── README.md

Esta estructura de archivos está organizada para crear una Acción de GitHub modular y mantenible:

  • login/ – Un directorio dedicado para la acción de autenticación, manteniendo los archivos relacionados juntos
  • index.js – El código principal de la acción que contiene la lógica de autenticación e interacciones con la API
  • action.yml – Define la interfaz de la acción, incluyendo entradas requeridas y cómo ejecutar la acción
  • package.json – Gestiona dependencias y metadatos del proyecto
  • README.md – Documentación para los usuarios de la acción

Esta estructura sigue las mejores prácticas para las Acciones de GitHub, manteniendo el código organizado y facilitando su mantenimiento y actualización a lo largo del tiempo.

Creando el Código de la Acción

Primero, debes crear el código de la acción. Esto implica crear el archivo principal de JavaScript que manejará la lógica de autenticación:

  1. Crea index.js – aquí es donde vive la lógica principal de la acción:
// 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();

El código utiliza el paquete @actions/core del toolkit de GitHub para manejar entradas, salidas y registro. También hemos implementado un manejo robusto de errores y registro para facilitar la depuración.

¡No te preocupes demasiado por entender todos los detalles del código JavaScript aquí! El punto clave es que este código de GitHub Action solo necesita hacer una cosa principal: usar core.setOutput() para devolver el token de autenticación.

Si no te sientes cómodo escribiendo este JavaScript tú mismo, puedes usar herramientas como ChatGPT para ayudar a generar el código. La parte más importante es entender que la acción necesita:

  • Obtener los valores de entrada (como la URL del servidor y las credenciales)
  • Hacer la solicitud de autenticación
  • Devolver el token usando core.setOutput()

Creación del paquete NodeJS

Ahora que entendemos la estructura del código y la funcionalidad de nuestra acción, vamos a configurar el paquete Node.js. Esto implica crear los archivos necesarios del paquete e instalar las dependencias que nuestra acción necesitará para funcionar correctamente.

  1. Crear package.json para definir nuestras dependencias y otros metadatos de acción.
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "Acción de GitHub para autenticar en el Servidor de Devolutions",
        "main": "index.js",
        "scripts": {
            "test": "echo \\"Error: no se especificaron pruebas\\" && exit 1"
        },
        "keywords": [
            "devolutions_server"
        ],
        "author": "Adam Bertram",
        "license": "MIT",
        "dependencies": {
            "@actions/core": "^1.10.1",
            "axios": "^1.6.7"
        }
    }
    
  2. Instalar dependencias ejecutando npm install.
    npm install
    

    Después de instalar las dependencias, deberías ver un nuevo directorio node_modules creado en la carpeta de tu proyecto. Este directorio contiene todos los paquetes necesarios que tu acción necesita para ejecutarse.

    Nota: Aunque vamos a confirmar package.json y package-lock.json al control de versiones, eventualmente excluiremos el directorio node_modules utilizando ncc para empaquetar nuestras dependencias.

  3. Cree action.yml para definir la interfaz de la acción:
    name: 'Inicio de sesión en Devolutions Server'
    description: 'Autenticar y obtener un token del servidor de Devolutions'
    inputs:
      server_url:
        description: 'URL del servidor de Devolutions'
        required: true
      app_key:
        description: 'Clave de la aplicación para la autenticación'
        required: true
      app_secret:
        description: 'Secreto de la aplicación para la autenticación'
        required: true
      output_variable:
        description: 'Nombre de la variable de entorno para almacenar el token recuperado'
        required: false
        default: 'DVLS_TOKEN'
    runs:
      using: 'node20'
      main: 'index.js'
    

    El archivo action.yml es crucial ya que define cómo funcionará tu acción dentro de los flujos de trabajo de GitHub Actions. Vamos a desglosar sus componentes clave:

    • name y description: Proporcionan información básica sobre lo que hace tu acción
    • inputs: Define los parámetros que los usuarios pueden pasar a tu acción:
      • server_url: Dónde encontrar el servidor de Devolutions
      • app_key y app_secret: Credenciales de autenticación
      • output_variable: Dónde almacenar el token resultante
    • runs: Especifica cómo ejecutar la acción:
      • using: 'node20': Utiliza la versión 20 de Node.js
      • main: 'index.js': Apunta al archivo JavaScript principal

    Cuando los usuarios hacen referencia a esta acción en sus flujos de trabajo, proporcionarán estos inputs de acuerdo con esta definición de interfaz.

Optimizando la acción

Para hacer que nuestra acción sea más mantenible y eficiente, utilizaremos el compilador ncc de Vercel para agrupar todas las dependencias en un solo archivo. Esto elimina la necesidad de comprometer el directorio node_modules:

Incluir node_modules en tu repositorio de GitHub Action no se recomienda por varias razones:

  • El directorio node_modules puede ser muy grande, conteniendo todas las dependencias y sus sub-dependencias, lo que inflaría innecesariamente el tamaño del repositorio
  • Diferentes sistemas operativos y entornos pueden manejar node_modules de manera diferente, lo que potencialmente podría causar problemas de compatibilidad
  • Usar el compilador ncc de Vercel para agrupar todas las dependencias en un solo archivo es un enfoque mejor porque:
    • Crea una acción más eficiente y mantenible
    • Elimina la necesidad de comprometer el directorio node_modules
  1. Instalar ncc:
    npm i -g @vercel/ncc
    
  2. Construir la versión agrupada:
    ncc build index.js --license licenses.txt
    
  3. Actualizar action.yml para apuntar al archivo agrupado:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Actualizado para usar la versión agrupada
    
  4. Limpiar:
    rm -rf node_modules  # Eliminar el directorio node_modules
    
  5. Comprometer los archivos en el repositorio compartido.
    git add .
    git commit -m "Commit inicial de la acción de inicio de sesión DVLS"
    git push
    

Creando el README

¡A todos les encanta la documentación, ¿verdad? ¿No? Bueno, a mí tampoco, así que he creado una plantilla de README para que la uses. Asegúrate de completar esto e incluirlo con tu acción.

# 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:

pasos:

  • nombre: Paso previo
    usos: ejemplo/nombre-accion@v1
    con:
    inputnombre: ${{ 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:

pasos:

  • nombre: Nombre del Paso
    utiliza: tu-org/nombre-accion@v1
    con:
    entradanombre: ‘Valor de Entrada’
    otra
    entrada: ‘Otro Valor’
## Example Workflow

Here's a complete example workflow utilizing this action:

nombre: Ejemplo de Flujo de Trabajo
en: [push]

tareas:
tarea-ejemplo:
se-ejecuta-en: ubuntu-latest
pasos:
– nombre: Revisar Repositorio
utiliza: 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).

Puntos Clave a Recordar

A la hora de crear tu propia acción personalizada:

  1. Siempre implementa un manejo de errores detallado y registro de eventos
  2. Utiliza el paquete @actions/core para una integración adecuada con las Acciones de GitHub
  3. Agrupa las dependencias con ncc para mantener limpio el repositorio
  4. Documenta claramente las entradas y salidas en tu action.yml
  5. Considera las implicaciones de seguridad y enmascara valores sensibles usando core.setSecret()

Esta acción de autenticación será utilizada por nuestra próxima acción que obtiene secretos. Continuemos con la creación de esa acción.

Paso 3: Creando la Acción “Obtener Secreto”

Has hecho el trabajo duro hasta este punto. Ahora sabes cómo crear una acción personalizada de Github. Si estás siguiendo, ahora necesitas repetir esos pasos para la acción de entrada del secreto DVLS de la siguiente manera:

La Estructura de la Acción

dvls-actions/
├── get-secret-entry/
│   ├── index.js
│   ├── action.yml
│   ├── package.json
│   └── README.md

El Archivo 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'

Optimizando la Acción

  1. Compila el archivo index.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. Actualiza action.yml para que apunte al archivo empaquetado:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Actualizado para usar la versión empaquetada
    
  3. Limpieza:
    rm -rf node_modules  # Eliminar el directorio node_modules
    
  4. Compromete los archivos al repositorio compartido.
    git add .
    git commit -m "Compromiso inicial de la acción de entrada secreta DVLS"
    git push
    

El Resultado Final

En este punto, deberías tener dos repositorios de GitHub:

  • el repositorio que contiene el flujo de trabajo que tenías usando secretos de GitHub
  • el repositorio compartido (asumiendo que el nombre es dvls-actions) que contiene las dos acciones con una estructura que se ve así:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

Usando las Acciones Personalizadas

Una vez que hayas configurado estas acciones personalizadas, puedes usarlas en tu flujo de trabajo original de llamada.

Flujo de trabajo original:

  • Utiliza un único paso para enviar una notificación a Slack
  • Referencia directamente la URL del webhook desde los secretos (secrets.SLACK_WEBHOOK_URL)

Nuevo flujo de trabajo:

  • Agrega un paso de autenticación usando la acción de inicio de sesión personalizada DVLS
  • Recupera de manera segura la URL del webhook de Slack desde el Servidor de Devolutions
  • Utiliza variables de entorno en lugar de secretos
  • Mantiene la misma funcionalidad de notificación pero con una seguridad mejorada

El nuevo flujo de trabajo añade dos pasos antes de la notificación de Slack:

  1. Autenticación con el Servidor de Devolutions usando la acción dvls-login
  2. Recuperación de la URL del webhook de Slack usando la acción dvls-get-secret-entry
  3. El paso final de notificación de Slack sigue siendo similar pero utiliza la URL del webhook recuperada de una variable de entorno (env.SLACK_WEBHOOK_URL)
name: Release Notification
on:
  release:
    types: [published]

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Login to Devolutions Server
        uses: devolutions-community/dvls-login@main
        with:
          server_url: 'https://1.1.1.1/dvls'
          app_key: ${{ vars.DVLS_APP_KEY }}
          app_secret: ${{ vars.DVLS_APP_SECRET }}

      - name: Get Slack Webhook URL
        uses: devolutions-community/dvls-get-secret-entry@main
        with:
          server_url: 'https://1.1.1.1/dvls'
          token: ${{ env.DVLS_TOKEN }}
          vault_name: 'DevOpsSecrets'
          entry_name: 'slack-webhook'
          output_variable: 'SLACK_WEBHOOK_URL'

      - name: Send Slack Notification
        run: |
          curl -X POST ${{ env.SLACK_WEBHOOK_URL }} \
          -H "Content-Type: application/json" \
          --data '{
            "text": "New release ${{ github.event.release.tag_name }} published!",
            "username": "GitHub Release Bot",
            "icon_emoji": ":rocket:"
          }'

Crear acciones personalizadas de GitHub permite estandarizar y asegurar tus flujos de trabajo en múltiples repositorios. Al mover operaciones sensibles como autenticación y recuperación de secretos en acciones dedicadas, puedes:

  • Mantener mejores prácticas de seguridad centralizando la gestión de credenciales
  • Reducir la duplicación de código en diferentes flujos de trabajo
  • Simplificar el mantenimiento y actualizaciones de los flujos de trabajo
  • Garantizar una implementación consistente de operaciones críticas

El ejemplo de integrar el Servidor de Devolutions con las Acciones de GitHub demuestra cómo las acciones personalizadas pueden cerrar la brecha entre diferentes herramientas mientras se mantienen las mejores prácticas de seguridad. Este enfoque se puede adaptar para diversas integraciones y casos de uso en tus flujos de trabajo de DevOps.

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