Creazione di Azioni GitHub Personalizzate: Una Guida Completa per i Team DevOps

Ti sei mai trovato a copiare e incollare lo stesso codice su più flussi di lavoro di GitHub? Quando hai bisogno di eseguire la stessa attività in repository o flussi di lavoro diversi, creare un’azione GitHub condivisa è la soluzione migliore. In questo tutorial, imparerai come creare da zero un’azione GitHub JavaScript personalizzata che puoi condividere all’interno della tua organizzazione.

Comprensione delle Azioni e dei Flussi di Lavoro di GitHub

Prima di immergerci nella creazione di un’azione personalizzata, stabiliamo un po’ di contesto. Un flusso di lavoro di GitHub è un processo automatizzato che puoi configurare nel tuo repository per compilare, testare, impacchettare, rilasciare o distribuire qualsiasi progetto su GitHub. Questi flussi di lavoro sono composti da uno o più job che possono essere eseguiti sequenzialmente o in parallelo.

Le Azioni di GitHub sono i singoli compiti che compongono un flusso di lavoro. Pensaci come blocchi predefiniti riutilizzabili: gestiscono compiti specifici come il controllo del codice, l’esecuzione dei test o il rilascio su un server. GitHub fornisce tre tipi di azioni:

  • Azioni del contenitore Docker
  • Azioni JavaScript
  • Azioni composite

In questo tutorial, ci concentreremo sulla creazione di un’azione JavaScript poiché viene eseguita direttamente sulla macchina del runner e può essere eseguita rapidamente.

Il Problema: Quando Creare un’Azione Personalizzata

Esploriamo quando e perché vorresti creare un’Azione GitHub personalizzata attraverso un esempio pratico. In tutto questo tutorial, utilizzeremo uno scenario specifico – l’integrazione con Devolutions Server (DVLS) per la gestione dei segreti – per dimostrare il processo, ma i concetti si applicano a qualsiasi situazione in cui hai bisogno di creare un’azione condivisa e riutilizzabile.

💡 Nota: Se disponi di Devolutions Server (DVLS) e desideri passare direttamente alla parte relativa all’uso, puoi trovare la versione completata nel repository Devolutions Github Actions.

Immagina di gestire più flussi di lavoro di GitHub che devono interagire con un servizio esterno – nel nostro esempio, recuperare segreti da DVLS. Ogni flusso di lavoro che ha bisogno di questa funzionalità richiede gli stessi passaggi di base:

  1. Connettersi al servizio esterno
  2. Autenticarsi
  3. Eseguire operazioni specifiche
  4. Gestire i risultati

Senza un’azione condivisa, dovresti duplicare questo codice in ogni flusso di lavoro. Non è solo inefficiente, ma è anche più difficile da mantenere e più soggetto a errori.

Perché Creare un’Azione Condivisa?

Creare un’azione condivisa di GitHub offre diversi vantaggi chiave che si applicano a qualsiasi scenario di integrazione:

  • Riutilizzabilità del Codice: Scrivi il codice di integrazione una volta e usalo in più flussi di lavoro e repository
  • Mantenibilità: Aggiorna l’azione in un unico punto per distribuire i cambiamenti ovunque sia utilizzata
  • Standardizzazione: Assicurati che tutti i team seguano lo stesso processo per compiti comuni
  • Controllo delle Versioni: Tieni traccia delle modifiche al codice di integrazione e torna indietro se necessario
  • Complessità Ridotta: Semplifica i flussi di lavoro astrattendo i dettagli di implementazione

Prerequisiti

Prima di iniziare questo tutorial, assicurati di avere quanto segue:

  • Un repository GitHub con un workflow esistente
  • Conoscenze di base su Git, inclusa la clonazione dei repository e la creazione di rami
  • Accesso come proprietario dell’organizzazione per creare e gestire repository condivisi
  • Comprensione di base di JavaScript e Node.js

Per il nostro scenario esemplificativo, creeremo un’azione che si integra con DVLS, ma puoi adattare i concetti a qualsiasi servizio esterno o funzionalità personalizzata di cui hai bisogno.

Cosa creerai

Alla fine di questo tutorial, comprenderai come:

  1. Creare un repository GitHub pubblico per azioni condivise
  2. Costruire più azioni interconnesse (creeremo due come esempi):
    • Una per gestire l’autenticazione
    • Un’altra per eseguire operazioni specifiche
  3. Creare un workflow che utilizzi le tue azioni personalizzate

Dimostreremo questi concetti costruendo azioni che si integrano con DVLS, ma puoi applicare gli stessi schemi per creare azioni per qualsiasi scopo di cui la tua organizzazione ha bisogno.

Punto di partenza: Il workflow esistente

Esaminiamo un semplice workflow che invia una notifica Slack quando viene creata una nuova release. Questo workflow attualmente utilizza i segreti di GitHub per memorizzare l’URL del webhook di 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:"
          }'

Notare il riferimento a secrets.SLACK_WEBHOOK_URL. Attualmente questo URL webhook è memorizzato come un segreto di GitHub, ma vogliamo recuperarlo invece dalla nostra istanza DVLS. Mentre questo è un semplice esempio che utilizza un solo segreto, immagina di avere decine di flussi di lavoro in tutta l’organizzazione, ognuno utilizzando più segreti. Gestire questi segreti in modo centralizzato in DVLS anziché sparsi su GitHub sarebbe molto più efficiente.

Piano di implementazione

Per convertire questo flusso di lavoro dall’uso dei segreti di GitHub a DVLS, dobbiamo:

  1. Preparare l’ambiente DVLS
    • Creare i corrispondenti segreti in DVLS
    • Testare gli endpoint API di DVLS per autenticazione e recupero dei segreti
  2. Creare il Repository delle Azioni Condivise
    • Costruire un’azione per l’autenticazione DVLS (dvls-login)
    • Costruire un’azione per il recupero dei valori segreti (dvls-get-secret-entry)
    • Utilizzare il compilatore ncc di Vercel per raggruppare le azioni senza node_modules
  3. Modificare il Flusso di Lavoro
    • Sostituire i riferimenti ai segreti di GitHub con le nostre azioni personalizzate
    • Testare la nuova implementazione

Ogni passaggio si basa sul precedente e alla fine avrai una soluzione riutilizzabile che qualsiasi flusso di lavoro nella tua organizzazione può sfruttare. Mentre stiamo usando DVLS come esempio, puoi adattare questo stesso modello a qualsiasi servizio esterno con cui i tuoi flussi di lavoro devono interagire.

Passaggio 1: Esplorare l’API Esterna

Prima di creare un’azione GitHub, è necessario capire come interagire con il servizio esterno. Per il nostro esempio DVLS, abbiamo bisogno di due segreti già configurati nell’istanza DVLS:

  • DVLS_APP_KEY – La chiave dell’applicazione per l’autenticazione
  • DVLS_APP_SECRET – Il segreto dell’applicazione per l’autenticazione

Testare il Flusso dell’API

Usiamo PowerShell per esplorare l’API DVLS e comprendere il flusso che dovremo implementare nella nostra azione. Questa fase di esplorazione è cruciale quando si crea un’azione personalizzata: è necessario comprendere i requisiti dell’API prima di implementarli.

$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

Questa esplorazione rivela il flusso dell’API che dovremo implementare nella nostra Azione GitHub:

  1. Autenticare con DVLS usando le credenziali dell’applicazione
  2. Ottenere le informazioni della cassetta di sicurezza utilizzando il token restituito
  3. Trovare l’ID dell’ingresso specifico per il nostro segreto
  4. Recuperare il valore effettivo del segreto

Comprendere questo flusso è cruciale perché dovremo implementare gli stessi passaggi nella nostra Azione GitHub, utilizzando JavaScript invece di PowerShell.

Quando crei la tua azione personalizzata, seguirai un processo simile:

  1. Identifica i punti finali dell’API con cui devi interagire
  2. Testa il processo di autenticazione e il recupero dei dati
  3. Documenta i passaggi che dovrai implementare nella tua azione

Passo 2: Creazione dell’Azione di Autenticazione

Ora che comprendiamo il flusso dell’API, creiamo la nostra prima azione personalizzata per gestire l’autenticazione. Costruiremo questo in un nuovo repository condiviso.

Configurazione della Struttura dell’Azione

Prima, crea la seguente struttura dei file nel tuo repository:

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

Questa struttura dei file è organizzata per creare un’azione modulare e manutenibile su GitHub:

  • login/ – Una directory dedicata per l’azione di autenticazione, mantenendo i file correlati insieme
  • index.js – Il codice principale dell’azione che contiene la logica di autenticazione e le interazioni con l’API
  • action.yml – Definisce l’interfaccia dell’azione, inclusi gli input richiesti e come eseguire l’azione
  • package.json – Gestisce le dipendenze e i metadati del progetto
  • README.md – Documentazione per gli utenti dell’azione

Questa struttura segue le migliori pratiche per le GitHub Actions, mantenendo il codice organizzato e facilitando la manutenzione e l’aggiornamento dell’azione nel tempo.

Creazione del Codice dell’Azione

Prima di tutto, devi creare il codice dell’azione. Questo coinvolge la creazione del file JavaScript principale che gestirà la logica di autenticazione:

  1. Crea index.js – qui risiede la logica principale dell’azione:
// 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();

Il codice utilizza il pacchetto @actions/core della toolkit di GitHub per gestire input, output e logging. Abbiamo anche implementato un robusto sistema di gestione degli errori e logging per semplificare il debug.

Non preoccuparti troppo di capire tutti i dettagli del codice JavaScript qui! Il punto chiave è che questo codice di azione di GitHub deve semplicemente fare una cosa principale: utilizzare core.setOutput() per restituire il token di autenticazione.

Se non ti senti a tuo agio a scrivere questo JavaScript da solo, puoi utilizzare strumenti come ChatGPT per aiutarti a generare il codice. La parte più importante è capire che l’azione deve:

  • Ottenere i valori di input (come URL del server e credenziali)
  • Effettuare la richiesta di autenticazione
  • Restituire il token utilizzando core.setOutput()

Creazione del pacchetto NodeJS

Ora che comprendiamo la struttura del codice e la funzionalità della nostra azione, impostiamo la configurazione del pacchetto Node.js. Questo implica la creazione dei file del pacchetto necessari e l’installazione delle dipendenze di cui l’azione avrà bisogno per funzionare correttamente.

  1. Crea package.json per definire le nostre dipendenze e altri metadati dell’azione.
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "Azione GitHub per autenticarsi a Devolutions Server",
        "main": "index.js",
        "scripts": {
            "test": "echo \\"Errore: nessun test specificato\\" && exit 1"
        },
        "keywords": [
            "devolutions_server"
        ],
        "author": "Adam Bertram",
        "license": "MIT",
        "dependencies": {
            "@actions/core": "^1.10.1",
            "axios": "^1.6.7"
        }
    }
    
  2. Installa le dipendenze eseguendo npm install.
    npm install
    

    Dopo aver installato le dipendenze, dovresti vedere una nuova cartella node_modules creata nella cartella del tuo progetto. Questa cartella contiene tutti i pacchetti richiesti affinché la tua azione possa funzionare.

    Nota: Sebbene committeremo package.json e package-lock.json nel controllo versione, escluderemo alla fine la cartella node_modules utilizzando ncc per raggruppare le nostre dipendenze.

  3. Crea action.yml per definire l’interfaccia dell’azione:
    nome: 'Accesso al Server Devolutions'
    descrizione: 'Autenticazione e ottenimento di un token dal Server Devolutions'
    input:
      server_url:
        descrizione: 'URL del Server Devolutions'
        richiesto: true
      app_key:
        descrizione: 'Chiave dell'applicazione per l'autenticazione'
        richiesto: true
      app_secret:
        descrizione: 'Segreto dell'applicazione per l'autenticazione'
        richiesto: true
      output_variable:
        descrizione: 'Nome della variabile d'ambiente per memorizzare il token ottenuto'
        richiesto: false
        predefinito: 'DVLS_TOKEN'
    esecuzione:
      utilizzando: 'node20'
      principale: 'index.js'
    

    Il file action.yml è cruciale poiché definisce come funzionerà la tua azione all’interno dei flussi di lavoro di GitHub Actions. Analizziamo i suoi componenti chiave:

    • nome e descrizione: Queste forniscono informazioni di base su cosa fa la tua azione
    • input: Definisce i parametri che gli utenti possono passare alla tua azione:
      • server_url: Dove trovare il Server Devolutions
      • app_key e app_secret: Credenziali di autenticazione
      • output_variable: Dove memorizzare il token risultante
    • esecuzione: Specifica come eseguire l’azione:
      • utilizzando: 'node20': Usa Node.js versione 20
      • principale: 'index.js': Indica il file JavaScript principale

    Quando gli utenti fanno riferimento a questa azione nei loro flussi di lavoro, forniranno questi input secondo questa definizione dell’interfaccia.

Ottimizzazione dell’azione

Per rendere la nostra azione più mantenibile ed efficiente, utilizzeremo il compilatore ncc di Vercel per raggruppare tutte le dipendenze in un singolo file. Ciò elimina la necessità di aggiungere la directory node_modules al repository dell’azione GitHub:

Includere node_modules nel repository dell’azione GitHub non è consigliato per diversi motivi:

  • La directory node_modules può essere molto grande, contenendo tutte le dipendenze e le relative sotto-dipendenze, il che gonfierebbe inutilmente le dimensioni del repository
  • I diversi sistemi operativi e ambienti potrebbero gestire node_modules in modo diverso, causando potenzialmente problemi di compatibilità
  • Utilizzare il compilatore ncc di Vercel per raggruppare tutte le dipendenze in un unico file è un approccio migliore perché:
    • Crea un’azione più efficiente e manutenibile
    • Elimina la necessità di eseguire il commit della directory node_modules
  1. Installa ncc:
    npm i -g @vercel/ncc
    
  2. Compila la versione raggruppata:
    ncc build index.js --license licenses.txt
    
  3. Aggiorna action.yml per puntare al file raggruppato:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Aggiornato per utilizzare la versione raggruppata
    
  4. Pulizia:
    rm -rf node_modules  # Rimuovi la directory node_modules
    
  5. Esegui il commit dei file nel repository condiviso.
    git add .
    git commit -m "Commit iniziale dell'azione di accesso a DVLS"
    git push
    

Creazione del README

Tutti amano la documentazione, vero? No? Beh, neanche io, quindi ho creato un modello di README per te da utilizzare. Assicurati di compilare questo e includerlo con la tua azione.

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

passaggi:

  • nome: Passaggio Prerequisito
    usa: esempio/nome-azione@v1
    con:
    inputnome: ${{ segreti.INPUTSEGRETO }}
## 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:

passaggi:

  • nome: Nome Passaggio
    utilizza: tua-organizzazione/nome-azione@v1
    con:
    inputnome: ‘Valore Input’
    un altro
    input: ‘Altro Valore’
## Example Workflow

Here's a complete example workflow utilizing this action:

nome: Esempio Workflow
su: [push]

lavori:
esempio-lavoro:
eseguito-su: ubuntu-latest
passaggi:
– nome: Checkout Repository
utilizza: 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).

Punti Chiave da Ricordare

Quando crei la tua azione personalizzata:

  1. Implementa sempre una gestione degli errori approfondita e il logging
  2. Utilizza il pacchetto @actions/core per un’integrazione corretta delle Azioni GitHub
  3. Raggruppa le dipendenze con ncc per mantenere pulito il repository
  4. Documenta chiaramente gli input e gli output nel tuo action.yml
  5. Considera le implicazioni sulla sicurezza e maschera i valori sensibili utilizzando core.setSecret()

Questa azione di autenticazione verrà utilizzata dalla nostra prossima azione che recupera i segreti. Passiamo ora alla creazione di quell’azione.

Passaggio 3: Creazione dell’Azione “Ottieni Segreto”

Hai fatto il lavoro difficile fino a questo punto. Ora sai come creare un’azione personalizzata di Github. Se stai seguendo, ora devi ripetere quei passaggi per l’azione di inserimento segreto DVLS come segue:

La Struttura dell’Azione

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

Il File 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'

Ottimizzazione dell’Azione

  1. Compilare il file index.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. Aggiorna action.yml per puntare al file incluso:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Aggiornato per utilizzare la versione inclusa
    
  3. Pulizia:
    rm -rf node_modules  # Rimuovere la directory node_modules
    
  4. Committa i file nel repository condiviso.
    git add .
    git commit -m "Commit iniziale dell'azione di inserimento segreto DVLS"
    git push
    

Risultato Finale

A questo punto, dovresti avere due repository GitHub:

  • il repository contenente il workflow che usavi con i segreti di GitHub
  • il repository condiviso (ipotizzando che il nome sia dvls-actions) contenente le due azioni con una struttura simile a questa:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

Utilizzo delle Azioni Personalizzate

Una volta configurate queste azioni personalizzate, puoi utilizzarle nel tuo workflow di chiamata originale.

Workflow originale:

  • Utilizza un singolo passaggio per inviare una notifica Slack
  • Referenzia direttamente l’URL del webhook dai segreti (secrets.SLACK_WEBHOOK_URL)

Nuovo workflow:

  • Aggiunge un passaggio di autenticazione utilizzando l’azione di login personalizzata DVLS
  • Recupera in modo sicuro l’URL del webhook Slack dal Server Devolutions
  • Utilizza le variabili d’ambiente invece dei segreti
  • Mantiene la stessa funzionalità di notifica ma con una sicurezza migliorata

Il nuovo flusso di lavoro aggiunge due passaggi prima della notifica Slack:

  1. Autenticazione con Devolutions Server utilizzando l’azione dvls-login
  2. Recupero dell’URL del webhook Slack utilizzando l’azione dvls-get-secret-entry
  3. Il passaggio finale della notifica Slack rimane simile ma utilizza l’URL del webhook recuperato da una variabile ambientale (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:"
          }'

Creare azioni GitHub personalizzate consente di standardizzare e proteggere i propri flussi di lavoro su più repository. Spostando operazioni sensibili come autenticazione e recupero di segreti in azioni dedicate, puoi:

  • Mantenere migliori pratiche di sicurezza centralizzando la gestione delle credenziali
  • Ridurre la duplicazione del codice tra diversi flussi di lavoro
  • Semplificare la manutenzione e gli aggiornamenti del flusso di lavoro
  • Garantire un’implementazione coerente delle operazioni critiche

L’esempio di integrazione di Devolutions Server con GitHub Actions dimostra come le azioni personalizzate possano colmare il divario tra diversi strumenti mantenendo le migliori pratiche di sicurezza. Questo approccio può essere adattato per varie altre integrazioni e casi d’uso nei tuoi flussi di lavoro DevOps.

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