Criando Ações Personalizadas no GitHub: Um Guia Completo para Equipes de DevOps

Já se viu copiando e colando o mesmo código em vários fluxos de trabalho do GitHub? Quando você precisa realizar a mesma tarefa em diferentes repositórios ou fluxos de trabalho, criar uma Ação GitHub compartilhada é o caminho a seguir. Neste tutorial, aprenda como construir uma Ação GitHub JavaScript personalizada do zero que você pode compartilhar em toda a sua organização.

Compreendendo Ações e Fluxos de Trabalho do GitHub

Antes de mergulhar na criação de uma ação personalizada, vamos estabelecer algum contexto. Um fluxo de trabalho do GitHub é um processo automatizado que você pode configurar em seu repositório para construir, testar, empacotar, lançar ou implantar qualquer projeto no GitHub. Esses fluxos de trabalho são compostos por um ou mais jobs que podem ser executados sequencialmente ou em paralelo.

As Ações do GitHub são as tarefas individuais que compõem um fluxo de trabalho. Pense nelas como blocos de construção reutilizáveis – elas lidam com tarefas específicas como verificar o código, executar testes ou implantar em um servidor. O GitHub fornece três tipos de ações:

  • Ações de contêiner Docker
  • Ações JavaScript
  • Ações compostas

Para este tutorial, iremos focar em criar uma ação JavaScript, pois ela é executada diretamente na máquina de execução e pode ser executada rapidamente.

O Problema: Quando Criar uma Ação Personalizada

Vamos explorar quando e por que você gostaria de criar uma Ação GitHub personalizada por meio de um exemplo prático. Ao longo deste tutorial, usaremos um cenário específico – integrando com o Servidor Devolutions (DVLS) para gerenciamento de segredos – para demonstrar o processo, mas os conceitos se aplicam a qualquer situação em que você precise criar uma ação compartilhada e reutilizável.

💡 Nota: Se você tiver o Servidor Devolutions (DVLS) e quiser pular para a parte de uso, você pode encontrar a versão completa no repositório de Ações do Github da Devolutions.

Imagine que você está gerenciando vários fluxos de trabalho do GitHub que precisam interagir com um serviço externo – em nosso exemplo, recuperando segredos do DVLS. Cada fluxo de trabalho que precisa dessa funcionalidade requer as mesmas etapas básicas:

  1. Conectar ao serviço externo
  2. Autenticar
  3. Realizar operações específicas
  4. Manipular os resultados

Sem uma ação compartilhada, você precisaria duplicar esse código em todos os fluxos de trabalho. Isso não é apenas ineficiente – também é mais difícil de manter e mais propenso a erros.

Por que Criar uma Ação Compartilhada?

A criação de uma Ação Compartilhada no GitHub oferece vários benefícios-chave que se aplicam a qualquer cenário de integração:

  • Reutilização de Código: Escreva o código de integração uma vez e use em vários fluxos de trabalho e repositórios
  • Manutenção: Atualize a ação em um só lugar para implementar alterações em todos os locais em que é usada
  • Padronização: Garanta que todas as equipes sigam o mesmo processo para tarefas comuns
  • Controle de Versão: Acompanhe as alterações no código de integração e reverta se necessário
  • Complexidade Reduzida: Simplifique os fluxos de trabalho abstraindo detalhes de implementação

Pré-requisitos

Antes de começar este tutorial, certifique-se de ter o seguinte em ordem:

  • Um repositório GitHub com um fluxo de trabalho existente
  • Conhecimento básico de Git, incluindo clonagem de repositórios e criação de branches
  • Acesso de proprietário da organização para criar e gerenciar repositórios compartilhados
  • Compreensão básica de JavaScript e Node.js

Para nosso cenário de exemplo, criaremos uma ação que se integra ao DVLS, mas você pode adaptar os conceitos para qualquer serviço externo ou funcionalidade personalizada que precisar.

O que você irá criar

No final deste tutorial, você entenderá como:

  1. Criar um repositório público no GitHub para ações compartilhadas
  2. Construir várias ações interconectadas (vamos criar duas como exemplos):
    • Uma para lidar com autenticação
    • Outra para realizar operações específicas
  3. Criar um fluxo de trabalho que utiliza suas ações personalizadas

Demonstrar esses conceitos construindo ações que se integram ao DVLS, mas você pode aplicar os mesmos padrões para criar ações para qualquer propósito que sua organização precisar.

Ponto de partida: O fluxo de trabalho existente

Vamos examinar um fluxo de trabalho simples que envia uma notificação no Slack quando uma nova versão é criada. Este fluxo de trabalho atualmente usa segredos do GitHub para armazenar a URL do webhook do 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:"
          }'

Observe a referência secrets.SLACK_WEBHOOK_URL. Esta URL de webhook está atualmente armazenada como um segredo do GitHub, mas queremos recuperá-la da nossa instância DVLS. Embora este seja um exemplo simples usando apenas um segredo, imagine ter dezenas de fluxos de trabalho em sua organização, cada um utilizando vários segredos. Gerenciar esses segredos centralmente no DVLS em vez de espalhados pelo GitHub seria muito mais eficiente.

Plano de Implementação

Para converter este fluxo de trabalho de usar segredos do GitHub para DVLS, precisamos:

  1. Preparar o Ambiente DVLS
    • Criar segredos correspondentes no DVLS
    • Testar os endpoints da API do DVLS para autenticação e recuperação de segredos
  2. Criar o Repositório de Ações Compartilhadas
    • Construir uma ação para autenticação no DVLS (dvls-login)
    • Construir uma ação para recuperar valores de segredos (dvls-get-secret-entry)
    • Usar o compilador ncc da Vercel para empacotar as ações sem node_modules
  3. Modificar o Fluxo de Trabalho
    • Substituir referências a segredos do GitHub por nossas ações personalizadas
    • Testar a nova implementação

Cada etapa se baseia na anterior e, ao final, você terá uma solução reutilizável que qualquer fluxo de trabalho em sua organização pode aproveitar. Embora estejamos usando o DVLS como exemplo, você pode adaptar esse mesmo padrão para qualquer serviço externo com o qual seus fluxos de trabalho precisem interagir.

Etapa 1: Explorando a API Externa

Antes de criar uma Ação do GitHub, você precisa entender como interagir com seu serviço externo. Para o nosso exemplo do DVLS, precisamos de duas chaves já configuradas na instância do DVLS:

  • DVLS_APP_KEY – A chave de aplicativo para autenticação
  • DVLS_APP_SECRET – O segredo do aplicativo para autenticação

Testando o Fluxo da API

Vamos usar o PowerShell para explorar a API do DVLS e entender o fluxo que precisaremos implementar em nossa ação. Essa fase de exploração é crucial ao criar qualquer ação personalizada – você precisa entender os requisitos da API antes de implementá-los.

$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

Essa exploração revela o fluxo da API que precisaremos implementar em nossa Ação do GitHub:

  1. Autenticar com o DVLS usando as credenciais do aplicativo
  2. Obter as informações do cofre usando o token retornado
  3. Localizar o ID específico da entrada para nosso segredo
  4. Recuperar o valor real do segredo

Entender este fluxo é crucial porque precisaremos implementar as mesmas etapas em nossa Ação do GitHub, apenas usando JavaScript em vez de PowerShell.

Ao criar sua própria ação personalizada, você seguirá um processo semelhante:

  1. Identifique os endpoints da API com os quais precisa interagir
  2. Teste o processo de autenticação e recuperação de dados
  3. Documente as etapas que você precisará implementar em sua ação

Etapa 2: Criando a Ação de Autenticação

Agora que entendemos o fluxo da API, vamos criar nossa primeira ação personalizada para lidar com a autenticação. Vamos construir isso em um novo repositório compartilhado.

Configurando a Estrutura da Ação

Primeiro, crie a seguinte estrutura de arquivos em seu repositório:

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

Esta estrutura de arquivos é organizada para criar uma Ação do GitHub modular e de fácil manutenção:

  • login/ – Um diretório dedicado para a ação de autenticação, mantendo arquivos relacionados juntos
  • index.js – O código principal da ação que contém a lógica de autenticação e interações com a API
  • action.yml – Define a interface da ação, incluindo entradas necessárias e como executar a ação
  • package.json – Gerencia dependências e metadados do projeto
  • README.md – Documentação para usuários da ação

Esta estrutura segue as melhores práticas para as Ações do GitHub, mantendo o código organizado e facilitando a manutenção e atualização da ação ao longo do tempo.

Criando o Código da Ação

Primeiramente, você deve criar o código da ação. Isso envolve a criação do arquivo JavaScript principal que lidará com a lógica de autenticação:

  1. Crie index.js – aqui é onde fica a lógica principal da ação:
// 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();

O código usa o pacote @actions/core do toolkit do GitHub para lidar com entradas, saídas e registros. Também implementamos um tratamento de erros robusto e registro para facilitar a depuração.

Não se preocupe muito em entender todos os detalhes do código JavaScript aqui! O ponto chave é que este código da GitHub Action precisa fazer uma coisa principal: usar core.setOutput() para retornar o token de autenticação.

Se você não se sente confortável escrevendo esse JavaScript sozinho, pode usar ferramentas como o ChatGPT para ajudar a gerar o código. A parte mais importante é entender que a ação precisa:

  • Obter os valores de entrada (como URL do servidor e credenciais)
  • Fazer a solicitação de autenticação
  • Retornar o token usando core.setOutput()

Criando o Pacote NodeJS

Agora que entendemos a estrutura do código e a funcionalidade da nossa ação, vamos configurar a configuração do pacote Node.js. Isso envolve criar os arquivos de pacote necessários e instalar as dependências que nossa ação precisará para funcionar corretamente.

  1. Crie package.json para definir nossas dependências e outros metadados de ação.
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "Ação do GitHub para autenticar no Servidor Devolutions",
        "main": "index.js",
        "scripts": {
            "test": "echo \\"Error: no test specified\\" && exit 1"
        },
        "keywords": [
            "devolutions_server"
        ],
        "author": "Adam Bertram",
        "license": "MIT",
        "dependencies": {
            "@actions/core": "^1.10.1",
            "axios": "^1.6.7"
        }
    }
    
  2. Instale dependências executando npm install.
    npm install
    

    Após instalar as dependências, você deverá ver um novo diretório node_modules criado na pasta do seu projeto. Este diretório contém todos os pacotes necessários para sua ação ser executada.

    Nota: Enquanto vamos commitar package.json e package-lock.json no controle de versão, eventualmente excluiremos o diretório node_modules usando ncc para agrupar nossas dependências.

  3. Crie action.yml para definir a interface da ação:
    name: 'Login no Servidor Devolutions'
    description: 'Autenticar e obter um token do Servidor Devolutions'
    inputs:
      server_url:
        description: 'URL do Servidor Devolutions'
        required: true
      app_key:
        description: 'Chave da aplicação para autenticação'
        required: true
      app_secret:
        description: 'Segredo da aplicação para autenticação'
        required: true
      output_variable:
        description: 'Nome da variável de ambiente para armazenar o token obtido'
        required: false
        default: 'DVLS_TOKEN'
    runs:
      using: 'node20'
      main: 'index.js'
    

    O arquivo action.yml é crucial, pois define como sua ação funcionará nos fluxos de trabalho do GitHub Actions. Vamos analisar seus principais componentes:

    • nome e descrição: Fornecem informações básicas sobre o que sua ação faz
    • inputs: Define os parâmetros que os usuários podem passar para sua ação:
      • server_url: Onde encontrar o Servidor Devolutions
      • app_key e app_secret: Credenciais de autenticação
      • output_variable: Onde armazenar o token resultante
    • runs: Especifica como executar a ação:
      • using: 'node20': Usa a versão Node.js 20
      • main: 'index.js': Aponta para o arquivo JavaScript principal

    Quando os usuários referenciam esta ação em seus fluxos de trabalho, eles fornecerão esses inputs de acordo com essa definição de interface.

Otimizando a Ação

Para tornar nossa ação mais mantida e eficiente, usaremos o compilador ncc da Vercel para agrupar todas as dependências em um único arquivo. Isso elimina a necessidade de comprometer o diretório node_modules:

Incluir node_modules em seu repositório de Ação do GitHub não é recomendado por várias razões:

  • O diretório node_modules pode ser muito grande, contendo todas as dependências e suas subdependências, o que inflaria desnecessariamente o tamanho do repositório
  • Diferentes sistemas operacionais e ambientes podem lidar com o node_modules de forma diferente, potencialmente causando problemas de compatibilidade
  • O uso do compilador ncc da Vercel para agrupar todas as dependências em um único arquivo é uma abordagem melhor porque:
    • Cria uma ação mais eficiente e fácil de manter
    • Elimina a necessidade de commitar o diretório node_modules
  1. Instale ncc:
    npm i -g @vercel/ncc
    
  2. Construa a versão agrupada:
    ncc build index.js --license licenses.txt
    
  3. Atualize o action.yml para apontar para o arquivo agrupado:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Atualizado para usar a versão agrupada
    
  4. Limpeza:
    rm -rf node_modules  # Remover o diretório node_modules
    
  5. Commit os arquivos para o repositório compartilhado.
    git add .
    git commit -m "Commit inicial da ação de login DVLS"
    git push
    

Criando o README

Todos amam documentação, certo? Não? Bem, eu também não, então criei um modelo de README para você usar. Certifique-se de preencher isso e incluir com sua ação.

# 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: Etapa Pré-Requisito
    uses: exemplo/nome-da-acao@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:

  • nome: Nome da Etapa
    usos: sua-org/nome-da-acao@v1
    com:
    entradanome: ‘Valor de Entrada’
    outro
    input: ‘Outro Valor’
## Example Workflow

Here's a complete example workflow utilizing this action:

nome: Exemplo do Fluxo de Trabalho
em: [push]

trabalhos:
exemplo-trabalho:
roda-em: ubuntu-ultima-versao
passos:
– nome: Verificar Repositório
usos: 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).

Pontos-chave a Lembrar

Ao criar sua própria ação personalizada:

  1. Implemente sempre um tratamento de erros detalhado e registro
  2. Use o pacote @actions/core para uma integração adequada com as Ações do GitHub
  3. Agrupe dependências com ncc para manter o repositório limpo
  4. Documente claramente as entradas e saídas em seu action.yml
  5. Considere as implicações de segurança e mascare valores sensíveis usando core.setSecret()

Esta ação de autenticação será usada pela nossa próxima ação que obtém segredos. Vamos seguir em frente e criar essa ação.

Etapa 3: Criando a Ação “Obter Segredo”

Você fez o trabalho difícil até este ponto. Agora você sabe como criar uma ação personalizada do Github. Se você estiver acompanhando, agora precisa repetir essas etapas para a entrada de segredo DVLS da ação, conforme a seguir:

A Estrutura da Ação

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

O Arquivo 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'

Otimizando a Ação

  1. Compilar o arquivo index.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. Atualize action.yml para apontar para o arquivo empacotado:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Atualizado para usar a versão empacotada
    
  3. Limpeza:
    rm -rf node_modules  # Remover o diretório node_modules
    
  4. Commitar os arquivos para o repositório compartilhado.
    git add .
    git commit -m "Commit inicial da ação de entrada de segredo do DVLS"
    git push
    

O Resultado Final

Neste ponto, você deve ter dois repositórios do GitHub:

  • o repositório contendo o fluxo de trabalho que você tinha usando segredos do GitHub
  • o repositório compartilhado (assumindo que o nome seja dvls-actions) contendo as duas ações com uma estrutura parecida com esta:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

Usando as Ações Personalizadas

Depois de configurar essas ações personalizadas, você pode usá-las em seu fluxo de trabalho de chamada original.

Fluxo de trabalho original:

  • Usa uma etapa única para enviar uma notificação no Slack
  • Faz referência diretamente à URL do webhook dos segredos (secrets.SLACK_WEBHOOK_URL)

Novo fluxo de trabalho:

  • Adiciona uma etapa de autenticação usando a ação de login personalizada do DVLS
  • Recupera a URL do webhook do Slack de forma segura do Servidor Devolutions
  • Usa variáveis de ambiente em vez de segredos
  • Mantém a mesma funcionalidade de notificação, mas com segurança aprimorada

O novo fluxo de trabalho adiciona dois passos antes da notificação no Slack:

  1. Autenticação com o Servidor de Devolutions usando a ação dvls-login
  2. Recuperação da URL do webhook do Slack usando a ação dvls-get-secret-entry
  3. A etapa final de notificação no Slack permanece semelhante, mas usa a URL do webhook recuperada de uma variável de ambiente (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:"
          }'

A criação de Ações personalizadas do GitHub permite padronizar e garantir a segurança de seus fluxos de trabalho em vários repositórios. Ao mover operações sensíveis como autenticação e recuperação de segredos para ações dedicadas, você pode:

  • Manter melhores práticas de segurança centralizando o gerenciamento de credenciais
  • Reduzir a duplicação de código em diferentes fluxos de trabalho
  • Simplificar a manutenção e atualizações do fluxo de trabalho
  • Garantir a implementação consistente de operações críticas

O exemplo de integração do Servidor de Devolutions com Ações do GitHub demonstra como ações personalizadas podem preencher a lacuna entre diferentes ferramentas, mantendo as melhores práticas de segurança. Esta abordagem pode ser adaptada para várias outras integrações e casos de uso em seus fluxos de trabalho de DevOps.

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