Erstellen von benutzerdefinierten GitHub-Aktionen: Ein kompletter Leitfaden für DevOps-Teams

Haben Sie jemals festgestellt, dass Sie denselben Code in mehreren GitHub-Workflows kopieren und einfügen? Wenn Sie dieselbe Aufgabe in verschiedenen Repositories oder Workflows ausführen müssen, ist die Erstellung einer gemeinsamen GitHub-Action der richtige Weg. In diesem Tutorial lernen Sie, wie Sie eine benutzerdefinierte JavaScript-GitHub-Action von Grund auf erstellen, die Sie in Ihrer Organisation teilen können.

Verständnis von GitHub Actions und Workflows

Bevor wir mit der Erstellung einer benutzerdefinierten Action beginnen, lassen Sie uns etwas Kontext schaffen. Ein GitHub-Workflow ist ein automatisierter Prozess, den Sie in Ihrem Repository einrichten können, um ein Projekt auf GitHub zu bauen, zu testen, zu paketieren, zu veröffentlichen oder bereitzustellen. Diese Workflows bestehen aus einem oder mehreren Jobs, die sequenziell oder parallel ausgeführt werden können.

GitHub Actions sind die einzelnen Aufgaben, die einen Workflow ausmachen. Denken Sie an sie als wiederverwendbare Bausteine – sie übernehmen spezifische Aufgaben wie das Auschecken von Code, das Ausführen von Tests oder das Bereitstellen auf einem Server. GitHub bietet drei Arten von Actions:

  • Docker-Container-Actionen
  • JavaScript-Actionen
  • Zusammengesetzte Actionen

Für dieses Tutorial konzentrieren wir uns auf die Erstellung einer JavaScript-Action, da sie direkt auf der Runner-Maschine ausgeführt wird und schnell ausgeführt werden kann.

Das Problem: Wann man eine benutzerdefinierte Action erstellen sollte

Lassen Sie uns erkunden, wann und warum Sie eine benutzerdefinierte GitHub-Action erstellen möchten, anhand eines praktischen Beispiels. Im Verlauf dieses Tutorials verwenden wir ein spezifisches Szenario – die Integration mit Devolutions Server (DVLS) zur Verwaltung von Geheimnissen – um den Prozess zu demonstrieren, aber die Konzepte gelten für jede Situation, in der Sie eine gemeinsame, wiederverwendbare Action erstellen müssen.

💡 Hinweis: Wenn Sie Devolutions Server (DVLS) haben und zum Nutzungsteil springen möchten, finden Sie die vollständige Version im Devolutions Github Actions Repository.

Stellen Sie sich vor, Sie verwalten mehrere GitHub-Workflows, die mit einem externen Dienst interagieren müssen – in unserem Beispiel das Abrufen von Geheimnissen von DVLS. Jeder Workflow, der diese Funktionalität benötigt, erfordert die gleichen grundlegenden Schritte:

  1. Mit dem externen Dienst verbinden
  2. Authentifizieren
  3. Bestimmte Operationen durchführen
  4. Die Ergebnisse verarbeiten

Ohne eine gemeinsame Aktion müssten Sie diesen Code in jedem Workflow duplizieren. Das ist nicht nur ineffizient – es ist auch schwieriger zu warten und anfälliger für Fehler.

Warum eine gemeinsame Aktion erstellen?

Die Erstellung einer gemeinsamen GitHub-Aktion bietet mehrere wichtige Vorteile, die auf jedes Integrationsszenario zutreffen:

  • Code-Wiederverwendbarkeit: Schreiben Sie den Integrationscode einmal und verwenden Sie ihn in mehreren Workflows und Repositories
  • Wartbarkeit: Aktualisieren Sie die Aktion an einem Ort, um Änderungen überall dort einzuführen, wo sie verwendet wird
  • Standardisierung: Stellen Sie sicher, dass alle Teams den gleichen Prozess für gemeinsame Aufgaben befolgen
  • Versionskontrolle: Verfolgen Sie Änderungen am Integrationscode und rollen Sie bei Bedarf zurück
  • Reduzierte Komplexität: Vereinfachen Sie Workflows, indem Sie Implementierungsdetails abstrahieren

Voraussetzungen

Bevor Sie mit diesem Tutorial beginnen, stellen Sie sicher, dass Sie Folgendes haben:

  • Ein GitHub-Repository mit einem bestehenden Workflow
  • Grundkenntnisse in Git, einschließlich Klonen von Repositories und Erstellen von Branches
  • Zugriff des Organisationsbesitzers, um gemeinsame Repositories zu erstellen und zu verwalten
  • Grundlegendes Verständnis von JavaScript und Node.js

Für unser Beispiel Szenario werden wir eine Aktion erstellen, die mit DVLS integriert, aber Sie können die Konzepte an jeden externen Dienst oder jede benutzerdefinierte Funktionalität anpassen, die Sie benötigen.

Was Sie erstellen werden

Am Ende dieses Tutorials werden Sie verstehen, wie Sie:

  1. Ein öffentliches GitHub-Repository für gemeinsame Aktionen erstellen
  2. Mehrere miteinander verbundene Aktionen erstellen (wir werden zwei als Beispiele erstellen):
    • Eine zur Authentifizierung
    • Eine andere zur Durchführung spezifischer Operationen
  3. Einen Workflow erstellen, der Ihre benutzerdefinierten Aktionen verwendet

Wir werden diese Konzepte demonstrieren, indem wir Aktionen erstellen, die mit DVLS integriert sind, aber Sie können dieselben Muster anwenden, um Aktionen für jeden Zweck zu erstellen, den Ihre Organisation benötigt.

Ausgangspunkt: Der bestehende Workflow

Werfen wir einen Blick auf einen einfachen Workflow, der eine Slack-Benachrichtigung sendet, wenn eine neue Version veröffentlicht wird. Dieser Workflow verwendet derzeit GitHub Secrets, um die Slack-Webhook-URL zu speichern:

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

Beachten Sie die Referenz secrets.SLACK_WEBHOOK_URL. Diese Webhook-URL wird derzeit als GitHub-Geheimnis gespeichert, aber wir möchten sie stattdessen von unserer DVLS-Instanz abrufen. Auch wenn dies nur ein einfaches Beispiel ist, bei dem nur ein Geheimnis verwendet wird, stellen Sie sich vor, dass Dutzende von Workflows in Ihrer Organisation vorhanden sind, von denen jeder mehrere Geheimnisse verwendet. Das zentrale Verwalten dieser Geheimnisse in DVLS anstelle von überall verstreut in GitHub wäre viel effizienter.

Implementierungsplan

Um diesen Workflow von der Verwendung von GitHub-Geheimnissen auf DVLS umzustellen, müssen wir:

  1. DVLS-Umgebung vorbereiten
    • Entsprechende Geheimnisse in DVLS erstellen
    • DVLS-API-Endpunkte für Authentifizierung und Geheimnisabruf testen
  2. Das Shared-Actions-Repository erstellen
    • Eine Aktion für die DVLS-Authentifizierung erstellen (dvls-login)
    • Eine Aktion zum Abrufen von Geheimniswerten erstellen (dvls-get-secret-entry)
    • Verwenden Sie den ncc-Compiler von Vercel, um die Aktionen ohne node_modules zu bündeln
  3. Den Workflow anpassen
    • Ersetzen Sie die Verweise auf GitHub-Geheimnisse durch unsere benutzerdefinierten Aktionen
    • Testen Sie die neue Implementierung

Jeder Schritt baut auf dem vorherigen auf, und am Ende haben Sie eine wiederverwendbare Lösung, die jede Arbeitsablauf in Ihrer Organisation nutzen kann. Während wir DVLS als Beispiel verwenden, können Sie dieses Muster auch für jeden externen Dienst anpassen, mit dem Ihre Arbeitsabläufe interagieren müssen.

Schritt 1: Erkunden der externen API

Bevor Sie eine GitHub-Aktion erstellen, müssen Sie verstehen, wie Sie mit Ihrem externen Dienst interagieren können. Für unser DVLS-Beispiel benötigen wir zwei bereits konfigurierte Secrets in der DVLS-Instanz:

  • DVLS_APP_KEY – Der Anwendungsschlüssel für die Authentifizierung
  • DVLS_APP_SECRET – Das Anwendungsgeheimnis für die Authentifizierung

Testen des API-Flusses

Verwenden wir PowerShell, um die DVLS-API zu erkunden und den Ablauf zu verstehen, den wir in unserer Aktion implementieren müssen. Diese Erkundungsphase ist entscheidend bei der Erstellung jeder benutzerdefinierten Aktion – Sie müssen die API-Anforderungen verstehen, bevor Sie sie implementieren können.

$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

Diese Erkundung zeigt den API-Fluss, den wir in unserer GitHub-Aktion implementieren müssen:

  1. Authentifizieren mit DVLS unter Verwendung der App-Anmeldeinformationen
  2. Die Tresorinformationen mit dem zurückgegebenen Token abrufen
  3. Die spezifische Eintrags-ID für unser Geheimnis lokalisieren
  4. Den tatsächlichen Geheimniswert abrufen

Das Verständnis dieses Ablaufs ist entscheidend, da wir dieselben Schritte in unserer GitHub-Aktion implementieren müssen, nur mit JavaScript anstelle von PowerShell.

Wenn Sie Ihre eigene benutzerdefinierte Aktion erstellen, werden Sie einen ähnlichen Prozess befolgen:

  1. Identifizieren Sie die API-Endpunkte, mit denen Sie interagieren müssen
  2. Testen Sie den Authentifizierungs- und Datenabrufprozess
  3. Dokumentieren Sie die Schritte, die Sie in Ihrer Aktion implementieren müssen

Schritt 2: Erstellen der Authentifizierungsaktion

Jetzt, da wir den API-Fluss verstehen, lassen Sie uns unsere erste benutzerdefinierte Aktion zur Handhabung der Authentifizierung erstellen. Wir werden dies in einem neuen gemeinsamen Repository aufbauen.

Einrichten der Aktionsstruktur

Erstellen Sie zunächst die folgende Verzeichnisstruktur in Ihrem Repository:

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

Diese Verzeichnisstruktur ist so organisiert, dass sie eine modulare und wartbare GitHub-Aktion erstellt:

  • login/ – Ein dediziertes Verzeichnis für die Authentifizierungsaktion, das verwandte Dateien zusammenführt
  • index.js – Der Hauptaktionscode, der die Authentifizierungslogik und API-Interaktionen enthält
  • action.yml – Definiert die Schnittstelle der Aktion, einschließlich erforderlicher Eingaben und wie man die Aktion ausführt
  • package.json – Verwaltet Abhängigkeiten und Projektmetadaten
  • README.md – Dokumentation für Benutzer der Aktion

Diese Struktur folgt bewährten Praktiken für GitHub-Aktionen, hält den Code organisiert und erleichtert die Wartung und Aktualisierung der Aktion im Laufe der Zeit.

Erstellen des Aktionscodes

Zuerst müssen Sie den Aktionscode erstellen. Dies beinhaltet die Erstellung der Haupt-JavaScript-Datei, die die Authentifizierungslogik behandelt:

  1. Erstellen Sie index.js – hier lebt die Hauptaktionslogik:
// 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();

Der Code verwendet das @actions/core Paket aus GitHub’s Toolkit, um Eingaben, Ausgaben und Protokollierung zu verwalten. Wir haben auch eine robuste Fehlerbehandlung und Protokollierung implementiert, um das Debuggen zu erleichtern.

Mach dir nicht zu viele Gedanken darüber, alle Details des JavaScript-Codes hier zu verstehen! Der entscheidende Punkt ist, dass dieser GitHub Action-Code nur eine Hauptsache tun muss: core.setOutput() verwenden, um das Authentifizierungstoken zurückzugeben.

Wenn du dich nicht wohlfühlst, diesen JavaScript-Code selbst zu schreiben, kannst du Tools wie ChatGPT verwenden, um den Code zu generieren. Der wichtigste Teil ist zu verstehen, dass die Aktion:

  • Die Eingabewerte (wie Server-URL und Anmeldedaten) abrufen
  • Die Authentifizierungsanfrage stellen
  • Das Token mit core.setOutput() zurückgeben

Das Erstellen des NodeJS-Pakets

Jetzt, da wir die Code-Struktur und Funktionalität unserer Aktion verstehen, lass uns die Konfiguration des Node.js-Pakets einrichten. Dies beinhaltet das Erstellen der notwendigen Paketdateien und das Installieren der Abhängigkeiten, die unsere Aktion benötigt, um ordnungsgemäß zu funktionieren.

  1. Erstellen Sie package.json, um unsere Abhängigkeiten und andere Aktionsmetadaten zu definieren.
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "GitHub-Aktion zur Authentifizierung am 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"
        }
    }
    
  2. Installieren Sie Abhängigkeiten, indem Sie npm install ausführen.
    npm install
    

    Nach der Installation von Abhängigkeiten sollte ein neues Verzeichnis node_modules in Ihrem Projektordner erstellt werden. Dieses Verzeichnis enthält alle erforderlichen Pakete, die Ihre Aktion zum Ausführen benötigt.

    Hinweis: Während wir package.json und package-lock.json ins Versionskontrollsystem übernehmen werden, werden wir letztendlich das Verzeichnis node_modules ausschließen, indem wir ncc verwenden, um unsere Abhängigkeiten zu bündeln.

  3. Erstellen Sie action.yml, um die Schnittstelle der Aktion zu definieren:
    name: 'Devolutions Server Login'
    description: 'Authentifizieren und ein Token vom Devolutions Server abrufen'
    inputs:
      server_url:
        description: 'URL des Devolutions Servers'
        required: true
      app_key:
        description: 'Anwendungsschlüssel für die Authentifizierung'
        required: true
      app_secret:
        description: 'Anwendungsgeheimnis für die Authentifizierung'
        required: true
      output_variable:
        description: 'Name der Umgebungsvariable zum Speichern des abgerufenen Tokens'
        required: false
        default: 'DVLS_TOKEN'
    runs:
      using: 'node20'
      main: 'index.js'
    

    Die action.yml-Datei ist entscheidend, da sie definiert, wie Ihre Aktion innerhalb von GitHub Actions-Workflows funktioniert. Lassen Sie uns die Schlüsselkomponenten aufschlüsseln:

    • Name und Beschreibung: Diese geben grundlegende Informationen darüber, was Ihre Aktion tut
    • Eingaben: Definiert die Parameter, die Benutzer Ihrer Aktion übergeben können:
      • server_url: Wo der Devolutions Server zu finden ist
      • app_key und app_secret: Authentifizierungsinformationen
      • output_variable: Wo das resultierende Token gespeichert werden soll
    • Ausführung: Legt fest, wie die Aktion ausgeführt wird:
      • using: 'node20': Verwendet Node.js Version 20
      • main: 'index.js': Verweist auf die Haupt-JavaScript-Datei

    Wenn Benutzer diese Aktion in ihren Workflows referenzieren, werden sie diese Eingaben gemäß dieser Schnittstellendefinition bereitstellen.

Optimierung der Aktion

Um unsere Aktion wartbarer und effizienter zu gestalten, werden wir den ncc-Compiler von Vercel verwenden, um alle Abhängigkeiten in eine einzige Datei zu bündeln. Dadurch entfällt die Notwendigkeit, das Verzeichnis node_modules zu commiten:

Das Einbeziehen von node_modules in Ihr GitHub-Aktionsrepository wird aus mehreren Gründen nicht empfohlen:

  • Das Verzeichnis node_modules kann sehr groß sein und enthält alle Abhängigkeiten und deren Unterabhängigkeiten, was die Repository-Größe unnötig aufbläht
  • Unterschiedliche Betriebssysteme und Umgebungen können node_modules unterschiedlich behandeln und potenziell Kompatibilitätsprobleme verursachen
  • Die Verwendung des Vercel ncc Compilers zum Bündeln aller Abhängigkeiten in eine einzige Datei ist ein besserer Ansatz, weil es:
    • Ein effizienteres und wartbares Vorgehen schafft
    • Den Bedarf an Commiting des node_modules Verzeichnisses beseitigt
  1. Installiere ncc:
    npm i -g @vercel/ncc
    
  2. Erstelle die gebündelte Version:
    ncc build index.js --license licenses.txt
    
  3. Aktualisiere die action.yml, um auf die gebündelte Datei zu verweisen:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Aktualisiert, um die gebündelte Version zu verwenden
    
  4. Aufräumen:
    rm -rf node_modules  # Entferne das node_modules Verzeichnis
    
  5. Committe die Dateien im gemeinsamen Repository.
    git add .
    git commit -m "Erster Commit der DVLS-Login-Aktion"
    git push
    

Erstellen des README

Jeder liebt Dokumentation, oder? Nein? Ich auch nicht, deshalb habe ich eine README-Vorlage für dich erstellt. Vergiss nicht, diese auszufüllen und mit deiner Aktion mitzuliefern.

# GitHub Action Template

This template provides a standardized structure for documenting any GitHub Action. Replace the placeholders with details specific to your action.

---

# Action Name

A brief description of what this GitHub Action does.

## Prerequisites

Outline any setup or configuration required before using the action. For example:

steps:

  • name: Voraussetzungsschritt
    uses: beispiel/aktionsname@v1
    with:
    inputname: ${{ secrets.INPUTSECRET }}
## Inputs

| Input Name       | Description                                    | Required | Default        |
|-------------------|------------------------------------------------|----------|----------------|
| `input_name`     | Description of the input parameter             | Yes/No   | Default Value  |
| `another_input`  | Description of another input parameter         | Yes/No   | Default Value  |

## Outputs

| Output Name      | Description                                    |
|-------------------|------------------------------------------------|
| `output_name`    | Description of the output parameter            |
| `another_output` | Description of another output parameter        |

## Usage

Provide an example of how to use this action in a workflow:

steps:

  • name: Schritt Name
    verwendet: your-org/action-name@v1
    mit:
    Eingabename: ‚Eingabewert‘
    eine weitere
    Eingabe: ‚Ein anderer Wert‘
## Example Workflow

Here's a complete example workflow utilizing this action:

Name: Beispielworkflow
bei: [push]

Jobs:
beispiel-job:
läuft auf: ubuntu-latest
Schritte:
– Name: Repository auschecken
verwendet: 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).

Wichtige Punkte, die zu beachten sind

Beim Erstellen Ihrer eigenen benutzerdefinierten Aktion:

  1. Implementieren Sie immer gründliche Fehlerbehandlung und Protokollierung
  2. Verwenden Sie das Paket @actions/core für eine ordnungsgemäße Integration von GitHub-Aktionen
  3. Bündeln Sie Abhängigkeiten mit ncc, um das Repository sauber zu halten
  4. Dokumentieren Sie Eingaben und Ausgaben klar in Ihrer action.yml
  5. Bedenken Sie Sicherheitsaspekte und maskieren Sie sensible Werte mit core.setSecret()

Diese Authentifizierungsaktion wird von unserer nächsten Aktion verwendet, die Geheimnisse abruft. Gehen wir nun zur Erstellung dieser Aktion über.

Schritt 3: Erstellen der Aktion „Geheimnis abrufen“

Sie haben bis hierher die harte Arbeit geleistet. Sie wissen jetzt, wie Sie eine benutzerdefinierte Github-Aktion erstellen. Wenn Sie mitmachen, müssen Sie diese Schritte nun für die DVLS-Eingabeaktion für das Geheimnis abrufen wie folgt wiederholen:

Die Aktionsstruktur

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

Die Datei 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();

Paket.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'

Optimierung der Aktion

  1. Kompilieren der Indexdatei.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. Aktualisieren Sie action.yml, um auf die gebündelte Datei zu verweisen:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Aktualisiert, um die gebündelte Version zu verwenden
    
  3. Aufräumen:
    rm -rf node_modules  # Entfernen Sie das Verzeichnis node_modules
    
  4. Übertragen Sie die Dateien in das gemeinsame Repository.
    git add .
    git commit -m "Erster Commit der DVLS get secret entry Aktion"
    git push
    

Das Endergebnis

Zu diesem Zeitpunkt sollten Sie zwei GitHub-Repos haben:

  • das Repo, das den Workflow enthält, den Sie mit GitHub-Secrets verwendet haben
  • das gemeinsame Repository (vorausgesetzt, der Name ist dvls-actions), das die beiden Aktionen mit einer Struktur wie folgt enthält:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

Verwendung der benutzerdefinierten Aktionen

Sobald Sie diese benutzerdefinierten Aktionen eingerichtet haben, können Sie sie in Ihrem ursprünglichen aufrufenden Workflow verwenden.

Ursprünglicher Workflow:

  • Verwendet einen einzelnen Schritt, um eine Slack-Benachrichtigung zu senden
  • Verweist direkt auf die Webhook-URL aus den Secrets (secrets.SLACK_WEBHOOK_URL)

Neuer Workflow:

  • Fügt einen Authentifizierungsschritt mit der benutzerdefinierten DVLS-Login-Aktion hinzu
  • Ruft die Slack Webhook-URL sicher vom Devolutions-Server ab
  • Verwendet Umgebungsvariablen anstelle von Secrets
  • Beibehaltung der gleichen Benachrichtigungsfunktionalität, jedoch mit verbesserter Sicherheit

Der neue Workflow fügt zwei Schritte vor der Slack-Benachrichtigung hinzu:

  1. Authentifizierung mit dem Devolutions Server über die dvls-login Aktion
  2. Abfrage der Slack-Webhooks-URL über die dvls-get-secret-entry Aktion
  3. Der letzte Schritt der Slack-Benachrichtigung bleibt ähnlich, verwendet jedoch die abgerufene Webhook-URL aus einer Umgebungsvariable (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:"
          }'

Das Erstellen benutzerdefinierter GitHub-Aktionen ermöglicht es Ihnen, Ihre Workflows über mehrere Repositories hinweg zu standardisieren und abzusichern. Durch die Verlagerung sensibler Vorgänge wie Authentifizierung und Geheimnisabfrage in dedizierte Aktionen können Sie:

  • Bessere Sicherheitspraktiken beibehalten, indem Sie das Credential-Management zentralisieren
  • Code-Duplikation über verschiedene Workflows hinweg reduzieren
  • Die Wartung und Aktualisierung von Workflows vereinfachen
  • Eine konsistente Implementierung kritischer Operationen sicherstellen

Das Beispiel der Integration des Devolutions Servers mit GitHub Actions zeigt, wie benutzerdefinierte Aktionen die Lücke zwischen verschiedenen Tools überbrücken können, während die besten Sicherheitspraktiken beibehalten werden. Dieser Ansatz kann für verschiedene andere Integrationen und Anwendungsfälle in Ihren DevOps-Workflows angepasst werden.

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