Création d’actions GitHub personnalisées : Guide complet pour les équipes DevOps

Vous êtes-vous déjà retrouvé à copier et coller le même code dans plusieurs workflows GitHub ? Lorsque vous devez effectuer la même tâche dans différents dépôts ou workflows, la création d’une action GitHub partagée est la solution idéale. Dans ce tutoriel, apprenez à construire une action GitHub JavaScript personnalisée depuis le début que vous pouvez partager au sein de votre organisation.

Comprendre les actions et workflows GitHub

Avant de plonger dans la création d’une action personnalisée, établissons un peu de contexte. Un workflow GitHub est un processus automatisé que vous pouvez configurer dans votre dépôt pour construire, tester, empaqueter, publier ou déployer n’importe quel projet sur GitHub. Ces workflows sont composés d’un ou plusieurs jobs qui peuvent s’exécuter de manière séquentielle ou en parallèle.

Les actions GitHub sont les tâches individuelles qui composent un workflow. Pensez-y comme à des blocs de construction réutilisables : elles gèrent des tâches spécifiques comme le checkout de code, l’exécution de tests ou le déploiement sur un serveur. GitHub propose trois types d’actions :

  • Actions de conteneur Docker
  • Actions JavaScript
  • Actions composites

Pour ce tutoriel, nous nous concentrerons sur la création d’une action JavaScript car elle s’exécute directement sur la machine du runner et peut s’exécuter rapidement.

Le Problème : Quand créer une action personnalisée

Explorons quand et pourquoi vous voudriez créer une action GitHub personnalisée à travers un exemple pratique. Tout au long de ce tutoriel, nous utiliserons un scénario spécifique – l’intégration avec Devolutions Server (DVLS) pour la gestion des secrets – pour démontrer le processus, mais les concepts s’appliquent à toute situation où vous devez créer une action partagée et réutilisable.

💡 Remarque : Si vous avez Devolutions Server (DVLS) et que vous souhaitez passer à la partie utilisation, vous pouvez trouver la version complète dans le référentiel Des actions Github de Devolutions.

Imaginez que vous gérez plusieurs flux de travail GitHub qui doivent interagir avec un service externe – dans notre exemple, récupérer des secrets de DVLS. Chaque flux de travail ayant besoin de cette fonctionnalité nécessite les mêmes étapes de base :

  1. Se connecter au service externe
  2. S’authentifier
  3. Effectuer des opérations spécifiques
  4. Gérer les résultats

Sans une action partagée, vous auriez besoin de dupliquer ce code dans chaque flux de travail. Ce n’est pas seulement inefficace – c’est aussi plus difficile à maintenir et plus sujet aux erreurs.

Pourquoi créer une action partagée ?

Créer une action GitHub partagée offre plusieurs avantages clés qui s’appliquent à tout scénario d’intégration :

  • Réutilisabilité du code : Écrivez le code d’intégration une fois et utilisez-le dans plusieurs flux de travail et dépôts
  • Maintenabilité : Mettez à jour l’action en un seul endroit pour déployer les modifications partout où elle est utilisée
  • Normalisation : Assurez-vous que toutes les équipes suivent le même processus pour les tâches courantes
  • Contrôle de version : Suivez les modifications apportées au code d’intégration et revenez en arrière si nécessaire
  • Complexité réduite : Simplifiez les flux de travail en abstrayant les détails d’implémentation

Prérequis

Avant de commencer ce tutoriel, assurez-vous d’avoir les éléments suivants en place :

  • Un dépôt GitHub avec un flux de travail existant
  • Des connaissances de base en Git, y compris le clonage de dépôts et la création de branches
  • Un accès en tant que propriétaire de l’organisation pour créer et gérer des dépôts partagés
  • Une compréhension de base de JavaScript et de Node.js

Pour notre exemple, nous allons créer une action qui s’intègre avec DVLS, mais vous pouvez adapter les concepts à tout service externe ou toute fonctionnalité personnalisée dont vous avez besoin.

Ce Que Vous Allez Créer

À la fin de ce tutoriel, vous saurez comment :

  1. Créer un dépôt GitHub public pour les actions partagées
  2. Construire plusieurs actions interconnectées (nous en créerons deux à titre d’exemple) :
    • Une pour gérer l’authentification
    • Une autre pour effectuer des opérations spécifiques
  3. Créer un flux de travail qui utilise vos actions personnalisées

Nous illustrerons ces concepts en construisant des actions qui s’intègrent avec DVLS, mais vous pouvez appliquer les mêmes schémas pour créer des actions pour tout besoin de votre organisation.

Point de Départ : Le Flux de Travail Existant

Examinons un flux de travail simple qui envoie une notification Slack lorsqu’une nouvelle version est créée. Ce flux de travail utilise actuellement des secrets GitHub pour stocker l’URL du webhook 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:"
          }'

Remarquez la référence secrets.SLACK_WEBHOOK_URL. Cette URL de webhook est actuellement stockée en tant que secret GitHub, mais nous souhaitons la récupérer depuis notre instance DVLS à la place. Bien qu’il s’agisse d’un exemple simple utilisant un seul secret, imaginez avoir des dizaines de flux de travail dans votre organisation, chacun utilisant plusieurs secrets. Gérer ces secrets de manière centralisée dans DVLS plutôt que de les disperser sur GitHub serait beaucoup plus efficace.

Plan de mise en œuvre

Pour convertir ce flux de travail de l’utilisation des secrets GitHub vers DVLS, nous devons :

  1. Préparer l’environnement DVLS
    • Créer les secrets correspondants dans DVLS
    • Tester les points de terminaison API DVLS pour l’authentification et la récupération des secrets
  2. Créer le dépôt d’actions partagées
    • Construire une action pour l’authentification DVLS (dvls-login)
    • Construire une action pour récupérer les valeurs secrètes (dvls-get-secret-entry)
    • Utiliser le compilateur ncc de Vercel pour regrouper les actions sans node_modules
  3. Modifier le flux de travail
    • Remplacer les références aux secrets GitHub par nos actions personnalisées
    • Tester la nouvelle mise en œuvre

Chaque étape se construit sur la précédente, et à la fin, vous aurez une solution réutilisable que n’importe quel flux de travail dans votre organisation peut exploiter. Bien que nous utilisions DVLS comme exemple, vous pouvez adapter ce même modèle pour n’importe quel service externe avec lequel vos flux de travail doivent interagir.

Étape 1 : Exploration de l’API externe

Avant de créer une Action GitHub, vous devez comprendre comment interagir avec votre service externe. Pour notre exemple DVLS, nous avons besoin de deux secrets déjà configurés dans l’instance DVLS :

  • DVLS_APP_KEY – La clé d’application pour l’authentification
  • DVLS_APP_SECRET – Le secret d’application pour l’authentification

Tester le flux de l’API

Utilisons PowerShell pour explorer l’API DVLS et comprendre le flux que nous devrons implémenter dans notre action. Cette phase d’exploration est cruciale lors de la création de toute action personnalisée – vous devez comprendre les exigences de l’API avant de les implémenter.

$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

Cette exploration révèle le flux de l’API que nous devrons implémenter dans notre Action GitHub :

  1. S’authentifier avec DVLS en utilisant les identifiants de l’application
  2. Obtenir les informations du coffre en utilisant le jeton renvoyé
  3. Localiser l’ID d’entrée spécifique pour notre secret
  4. Récupérer la valeur réelle du secret

Comprendre ce flux est crucial car nous devrons implémenter les mêmes étapes dans notre Action GitHub, simplement en utilisant JavaScript au lieu de PowerShell.

Lorsque vous créez votre propre action personnalisée, vous suivrez un processus similaire :

  1. Identifiez les points de terminaison API avec lesquels vous devez interagir
  2. Testez le processus d’authentification et de récupération de données
  3. Documentez les étapes que vous devrez mettre en œuvre dans votre action

Étape 2 : Création de l’Action d’Authentification

Maintenant que nous comprenons le flux de l’API, créons notre première action personnalisée pour gérer l’authentification. Nous allons construire cela dans un nouveau dépôt partagé.

Mise en place de la structure de l’action

Tout d’abord, créez la structure de fichiers suivante dans votre dépôt :

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

Cette structure de fichiers est organisée pour créer une Action GitHub modulaire et maintenable :

  • login/ – Un répertoire dédié à l’action d’authentification, regroupant les fichiers associés
  • index.js – Le code principal de l’action qui contient la logique d’authentification et les interactions API
  • action.yml – Définit l’interface de l’action, y compris les entrées requises et comment exécuter l’action
  • package.json – Gère les dépendances et les métadonnées du projet
  • README.md – Documentation pour les utilisateurs de l’action

Cette structure suit les meilleures pratiques pour les Actions GitHub, gardant le code organisé et facilitant la maintenance et la mise à jour de l’action au fil du temps.

Création du code de l’action

Tout d’abord, vous devez créer le code de l’action. Cela implique de créer le fichier JavaScript principal qui gérera la logique d’authentification :

  1. Créez index.js – c’est ici que réside la logique principale de l’action :
// 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();

Le code utilise le package @actions/core de la trousse à outils de GitHub pour gérer les entrées, les sorties et le journal. Nous avons également mis en place une gestion des erreurs robuste et un journal pour faciliter le débogage.

Ne vous inquiétez pas trop de comprendre tous les détails du code JavaScript ici! Le point clé est que ce code d’action GitHub doit simplement faire une chose principale : utiliser core.setOutput() pour renvoyer le jeton d’authentification.

Si vous n’êtes pas à l’aise pour écrire ce JavaScript vous-même, vous pouvez utiliser des outils comme ChatGPT pour vous aider à générer le code. La partie la plus importante est de comprendre que l’action doit :

  • Obtenir les valeurs d’entrée (comme l’URL du serveur et les informations d’identification)
  • Effectuer la demande d’authentification
  • Retourner le jeton en utilisant core.setOutput()

Création du package NodeJS

Maintenant que nous comprenons la structure du code et la fonctionnalité de notre action, configurons le package Node.js. Cela implique la création des fichiers de package nécessaires et l’installation des dépendances dont notre action aura besoin pour fonctionner correctement.

  1. Créez package.json pour définir nos dépendances et d’autres métadonnées d’action.
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "Action GitHub pour s'authentifier sur le serveur 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. Installez les dépendances en exécutant npm install.
    npm install
    

    Après avoir installé les dépendances, vous devriez voir un nouveau répertoire node_modules créé dans le dossier de votre projet. Ce répertoire contient tous les paquets requis pour que votre action puisse s’exécuter.

    Note : Bien que nous commettions package.json et package-lock.json dans le contrôle de version, nous exclurons éventuellement le répertoire node_modules en utilisant ncc pour regrouper nos dépendances.

  3. Créez action.yml pour définir l’interface de l’action :
    name: 'Connexion au serveur Devolutions'
    description: 'Authentifiez-vous et obtenez un jeton du serveur Devolutions'
    inputs:
      server_url:
        description: 'URL du serveur Devolutions'
        required: true
      app_key:
        description: 'Clé d’application pour l’authentification'
        required: true
      app_secret:
        description: 'Secret d’application pour l’authentification'
        required: true
      output_variable:
        description: 'Nom de la variable d’environnement pour stocker le jeton récupéré'
        required: false
        default: 'DVLS_TOKEN'
    runs:
      using: 'node20'
      main: 'index.js'
    

    Le fichier action.yml est crucial car il définit comment votre action fonctionnera dans les workflows de GitHub Actions. Décomposons ses principaux composants :

    • nom et description : Cela fournit des informations de base sur ce que fait votre action
    • inputs : Définit les paramètres que les utilisateurs peuvent passer à votre action :
      • server_url : Où trouver le serveur Devolutions
      • app_key et app_secret : Identifiants d’authentification
      • output_variable : Où stocker le jeton résultant
    • runs : Spécifie comment exécuter l’action :
      • using: 'node20' : Utilise la version 20 de Node.js
      • main: 'index.js' : Pointe vers le fichier JavaScript principal

    Lorsque les utilisateurs font référence à cette action dans leurs workflows, ils fourniront ces inputs selon cette définition d’interface.

Optimisation de l’action

Pour rendre notre action plus maintenable et efficace, nous utiliserons le compilateur ncc de Vercel pour regrouper toutes les dépendances dans un seul fichier. Cela élimine la nécessité de valider le répertoire node_modules:

Il n’est pas recommandé d’inclure node_modules dans le dépôt de votre action GitHub pour plusieurs raisons:

  • Le répertoire node_modules peut être très volumineux, contenant toutes les dépendances et leurs sous-dépendances, ce qui ferait gonfler inutilement la taille du dépôt
  • Différents systèmes d’exploitation et environnements peuvent gérer les node_modules de manière différente, entraînant potentiellement des problèmes de compatibilité
  • Utiliser le compilateur ncc de Vercel pour regrouper toutes les dépendances dans un seul fichier est une meilleure approche car cela :
    • Crée une action plus efficace et plus facile à maintenir
    • Élimine le besoin de commettre le répertoire node_modules
  1. Installer ncc :
    npm i -g @vercel/ncc
    
  2. Construire la version regroupée :
    ncc build index.js --license licenses.txt
    
  3. Mettre à jour action.yml pour pointer vers le fichier regroupé :
    runs :
      using: 'node20'
      main: 'dist/index.js'  # Mis à jour pour utiliser la version regroupée
    
  4. Nettoyer :
    rm -rf node_modules  # Supprimer le répertoire node_modules
    
  5. Commitez les fichiers dans le dépôt partagé.
    git add .
    git commit -m "Commit initial de l'action de connexion DVLS"
    git push
    

Créer le README

Tout le monde aime la documentation, n’est-ce pas ? Non ? Eh bien, moi non plus, alors j’ai créé un modèle de README pour que vous puissiez l’utiliser. Assurez-vous de le remplir et de l’inclure avec votre action.

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

étapes :

  • nom: Étape préalable
    utilise : example/action-name@v1
    avec:
    entréenom: ${{ 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:

étapes :

  • nom : Nom de l’étape
    utilisations : votre-org/action-name@v1
    avec:
    entréenom : ‘Valeur d’entrée’
    une autre
    entrée : ‘Une autre valeur’
## Example Workflow

Here's a complete example workflow utilizing this action:

nom : Exemple de workflow
sur : [push]

emplois :
exemple-emploi :
s’exécute sur : ubuntu-latest
étapes :
– nom : Vérifier le dépôt
utilisations : 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).

Points clés à retenir

Lors de la création de votre propre action personnalisée :

  1. Implémentez toujours une gestion d’erreurs approfondie et un journalisation
  2. Utilisez le package @actions/core pour une intégration correcte des actions GitHub
  3. Regroupez les dépendances avec ncc pour maintenir le dépôt propre
  4. Documentez clairement les entrées et les sorties dans votre action.yml
  5. Considérez les implications en matière de sécurité et masquez les valeurs sensibles en utilisant core.setSecret()

Cette action d’authentification sera utilisée par notre prochaine action qui récupère des secrets. Passons maintenant à la création de cette action.

Étape 3 : Création de l’action « Obtenir le secret »

Vous avez accompli le travail difficile jusqu’à présent. Vous savez maintenant comment créer une action Github personnalisée. Si vous suivez, vous devez maintenant répéter ces étapes pour l’action d’entrée de secret DVLS comme suit :

Structure de l’action

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

Fichier 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'

Optimisation de l’action

  1. Compiler le fichier index.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. Mettre à jour action.yml pour pointer vers le fichier groupé :
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Mis à jour pour utiliser la version groupée
    
  3. Nettoyer :
    rm -rf node_modules  # Supprimer le répertoire node_modules
    
  4. Valider les fichiers dans le dépôt partagé.
    git add .
    git commit -m "Validation initiale de l'action d'entrée secrète DVLS"
    git push
    

Résultat final

À ce stade, vous devriez avoir deux dépôts GitHub :

  • le dépôt contenant le flux de travail que vous aviez en utilisant les secrets GitHub
  • le dépôt partagé (en supposant que le nom est dvls-actions) contenant les deux actions avec une structure ressemblant à ceci :
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

Utilisation des actions personnalisées

Une fois que vous avez configuré ces actions personnalisées, vous pouvez les utiliser dans votre flux de travail d’appel d’origine.

Flux de travail d’origine :

  • Utilise une seule étape pour envoyer une notification Slack
  • Référence directement l’URL du webhook à partir des secrets (secrets.SLACK_WEBHOOK_URL)

Nouveau flux de travail :

  • Ajoute une étape d’authentification en utilisant l’action de connexion DVLS personnalisée
  • Récupère de manière sécurisée l’URL du webhook Slack depuis le serveur Devolutions
  • Utilise des variables d’environnement au lieu de secrets
  • Maintient la même fonctionnalité de notification mais avec une sécurité renforcée

Le nouveau flux de travail ajoute deux étapes avant la notification Slack :

  1. Authentification avec le serveur Devolutions en utilisant l’action dvls-login
  2. Récupération de l’URL du webhook Slack en utilisant l’action dvls-get-secret-entry
  3. L’étape finale de notification Slack reste similaire mais utilise l’URL du webhook récupérée à partir d’une variable d’environnement (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:"
          }'

La création d’actions GitHub personnalisées vous permet de standardiser et sécuriser vos flux de travail à travers plusieurs dépôts. En déplaçant des opérations sensibles telles que l’authentification et la récupération de secrets dans des actions dédiées, vous pouvez :

  • Maintenir de meilleures pratiques de sécurité en centralisant la gestion des identifiants
  • Réduire la duplication de code à travers différents flux de travail
  • Simplifier la maintenance et les mises à jour des flux de travail
  • Garantir une implémentation cohérente des opérations critiques

L’exemple de l’intégration du serveur Devolutions avec les actions GitHub montre comment les actions personnalisées peuvent combler l’écart entre différents outils tout en respectant les meilleures pratiques de sécurité. Cette approche peut être adaptée à diverses autres intégrations et cas d’utilisation dans vos flux de travail DevOps.

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