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:
- Mit dem externen Dienst verbinden
- Authentifizieren
- Bestimmte Operationen durchführen
- 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:
- Ein öffentliches GitHub-Repository für gemeinsame Aktionen erstellen
- Mehrere miteinander verbundene Aktionen erstellen (wir werden zwei als Beispiele erstellen):
- Eine zur Authentifizierung
- Eine andere zur Durchführung spezifischer Operationen
- 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:
- DVLS-Umgebung vorbereiten
- Entsprechende Geheimnisse in DVLS erstellen
- DVLS-API-Endpunkte für Authentifizierung und Geheimnisabruf testen
- 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
- Eine Aktion für die DVLS-Authentifizierung erstellen (
- 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 AuthentifizierungDVLS_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:
- Authentifizieren mit DVLS unter Verwendung der App-Anmeldeinformationen
- Die Tresorinformationen mit dem zurückgegebenen Token abrufen
- Die spezifische Eintrags-ID für unser Geheimnis lokalisieren
- 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:
- Identifizieren Sie die API-Endpunkte, mit denen Sie interagieren müssen
- Testen Sie den Authentifizierungs- und Datenabrufprozess
- 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:
- 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.
- 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" } }
- 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
undpackage-lock.json
ins Versionskontrollsystem übernehmen werden, werden wir letztendlich das Verzeichnisnode_modules
ausschließen, indem wirncc
verwenden, um unsere Abhängigkeiten zu bündeln. - 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 istapp_key
undapp_secret
: Authentifizierungsinformationenoutput_variable
: Wo das resultierende Token gespeichert werden soll
- Ausführung: Legt fest, wie die Aktion ausgeführt wird:
using: 'node20'
: Verwendet Node.js Version 20main: '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
- Installiere
ncc
:npm i -g @vercel/ncc
- Erstelle die gebündelte Version:
ncc build index.js --license licenses.txt
- 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
- Aufräumen:
rm -rf node_modules # Entferne das node_modules Verzeichnis
- 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 weitereEingabe: ‚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:
- Implementieren Sie immer gründliche Fehlerbehandlung und Protokollierung
- Verwenden Sie das Paket
@actions/core
für eine ordnungsgemäße Integration von GitHub-Aktionen - Bündeln Sie Abhängigkeiten mit
ncc
, um das Repository sauber zu halten - Dokumentieren Sie Eingaben und Ausgaben klar in Ihrer
action.yml
- 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
- Kompilieren der Indexdatei.
npm i -g @vercel/ncc ncc build index.js --license licenses.txt
- 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
- Aufräumen:
rm -rf node_modules # Entfernen Sie das Verzeichnis node_modules
- Ü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:
- Authentifizierung mit dem Devolutions Server über die
dvls-login
Aktion - Abfrage der Slack-Webhooks-URL über die
dvls-get-secret-entry
Aktion - 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/