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:
- Connettersi al servizio esterno
- Autenticarsi
- Eseguire operazioni specifiche
- 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:
- Creare un repository GitHub pubblico per azioni condivise
- Costruire più azioni interconnesse (creeremo due come esempi):
- Una per gestire l’autenticazione
- Un’altra per eseguire operazioni specifiche
- 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:
- Preparare l’ambiente DVLS
- Creare i corrispondenti segreti in DVLS
- Testare gli endpoint API di DVLS per autenticazione e recupero dei segreti
- 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
- Costruire un’azione per l’autenticazione DVLS (
- 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’autenticazioneDVLS_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:
- Autenticare con DVLS usando le credenziali dell’applicazione
- Ottenere le informazioni della cassetta di sicurezza utilizzando il token restituito
- Trovare l’ID dell’ingresso specifico per il nostro segreto
- 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:
- Identifica i punti finali dell’API con cui devi interagire
- Testa il processo di autenticazione e il recupero dei dati
- 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:
- 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.
- 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" } }
- 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
epackage-lock.json
nel controllo versione, escluderemo alla fine la cartellanode_modules
utilizzandoncc
per raggruppare le nostre dipendenze. - 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 Devolutionsapp_key
eapp_secret
: Credenziali di autenticazioneoutput_variable
: Dove memorizzare il token risultante
- esecuzione: Specifica come eseguire l’azione:
utilizzando: 'node20'
: Usa Node.js versione 20principale: '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
- Installa
ncc
:npm i -g @vercel/ncc
- Compila la versione raggruppata:
ncc build index.js --license licenses.txt
- Aggiorna
action.yml
per puntare al file raggruppato:runs: using: 'node20' main: 'dist/index.js' # Aggiornato per utilizzare la versione raggruppata
- Pulizia:
rm -rf node_modules # Rimuovi la directory node_modules
- 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 altroinput: ‘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:
- Implementa sempre una gestione degli errori approfondita e il logging
- Utilizza il pacchetto
@actions/core
per un’integrazione corretta delle Azioni GitHub - Raggruppa le dipendenze con
ncc
per mantenere pulito il repository - Documenta chiaramente gli input e gli output nel tuo
action.yml
- 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
- Compilare il file index.
npm i -g @vercel/ncc ncc build index.js --license licenses.txt
- Aggiorna
action.yml
per puntare al file incluso:runs: using: 'node20' main: 'dist/index.js' # Aggiornato per utilizzare la versione inclusa
- Pulizia:
rm -rf node_modules # Rimuovere la directory node_modules
- 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:
- Autenticazione con Devolutions Server utilizzando l’azione
dvls-login
- Recupero dell’URL del webhook Slack utilizzando l’azione
dvls-get-secret-entry
- 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/