Creating Custom GitHub Actions: A Complete Guide for DevOps Teams

Published:3 January 2025 - 17 min. read

Ever found yourself copying and pasting the same code across multiple GitHub workflows? When you need to perform the same task in different repositories or workflows, creating a shared GitHub Action is the way to go. In this tutorial, learn how to build a custom JavaScript GitHub Action from the ground up that you can share across your organization.

Understanding GitHub Actions and Workflows

Before diving into creating a custom action, let’s establish some context. A GitHub workflow is an automated process that you can set up in your repository to build, test, package, release, or deploy any project on GitHub. These workflows are made up of one or more jobs that can run sequentially or in parallel.

GitHub Actions are the individual tasks that make up a workflow. Think of them as reusable building blocks – they handle specific tasks like checking out code, running tests, or deploying to a server. GitHub provides three types of actions:

  • Docker container actions
  • JavaScript actions
  • Composite actions

For this tutorial, we’ll focus on creating a JavaScript action since it runs directly on the runner machine and can execute quickly.

The Problem: When to Create a Custom Action

Let’s explore when and why you’d want to create a custom GitHub Action through a practical example. Throughout this tutorial, we’ll use a specific scenario – integrating with Devolutions Server (DVLS) for secret management – to demonstrate the process, but the concepts apply to any situation where you need to create a shared, reusable action.

💡 Note: If you have Devolutions Server (DVLS) and would like to skip to the using part, you can find completed version in the Devolutions Github Actions repo.

Imagine you’re managing multiple GitHub workflows that need to interact with an external service – in our example, retrieving secrets from DVLS. Each workflow that needs this functionality requires the same basic steps:

  1. Connect to the external service
  2. Authenticate
  3. Perform specific operations
  4. Handle the results

Without a shared action, you’d need to duplicate this code across every workflow. That’s not just inefficient – it’s also harder to maintain and more prone to errors.

Why Create a Shared Action?

Creating a shared GitHub Action offers several key benefits that apply to any integration scenario:

  • Code Reusability: Write the integration code once and use it across multiple workflows and repositories
  • Maintainability: Update the action in one place to roll out changes everywhere it’s used
  • Standardization: Ensure all teams follow the same process for common tasks
  • Version Control: Track changes to the integration code and roll back if needed
  • Reduced Complexity: Simplify workflows by abstracting implementation details

Prerequisites

Before starting this tutorial, ensure you have the following in place:

  • A GitHub repository with an existing workflow
  • Basic Git knowledge, including cloning repositories and creating branches
  • Organization owner access to create and manage shared repositories
  • Basic understanding of JavaScript and Node.js

For our example scenario, we’ll create an action that integrates with DVLS, but you can adapt the concepts to any external service or custom functionality you need.

What You’ll Create

By the end of this tutorial, you’ll understand how to:

  1. Create a public GitHub repository for shared actions
  2. Build multiple interconnected actions (we’ll create two as examples):
    • One to handle authentication
    • Another to perform specific operations
  3. Create a workflow that uses your custom actions

We’ll demonstrate these concepts by building actions that integrate with DVLS, but you can apply the same patterns to create actions for any purpose your organization needs.

Starting Point: The Existing Workflow

Let’s examine a simple workflow that sends a Slack notification when a new release is created. This workflow currently uses GitHub secrets to store the Slack webhook URL:

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

Notice the secrets.SLACK_WEBHOOK_URL reference. This webhook URL is currently stored as a GitHub secret, but we want to retrieve it from our DVLS instance instead. While this is a simple example using just one secret, imagine having dozens of workflows across your organization, each using multiple secrets. Managing these secrets centrally in DVLS instead of scattered across GitHub would be much more efficient.

Implementation Plan

To convert this workflow from using GitHub secrets to DVLS, we need to:

  1. Prepare DVLS Environment
    • Create corresponding secrets in DVLS
    • Test DVLS API endpoints for authentication and secret retrieval
  2. Create the Shared Actions Repository
    • Build an action for DVLS authentication (dvls-login)
    • Build an action for retrieving secret values (dvls-get-secret-entry)
    • Use Vercel’s ncc compiler to bundle the actions without node_modules
  3. Modify the Workflow
    • Replace GitHub secrets references with our custom actions
    • Test the new implementation

Each step builds on the previous one, and by the end, you’ll have a reusable solution that any workflow in your organization can leverage. While we’re using DVLS as our example, you can adapt this same pattern for any external service your workflows need to interact with.

Step 1: Exploring the External API

Before creating a GitHub Action, you need to understand how to interact with your external service. For our DVLS example, we need two secrets already configured in the DVLS instance:

  • DVLS_APP_KEY – The application key for authentication
  • DVLS_APP_SECRET – The application secret for authentication

Testing the API Flow

Let’s use PowerShell to explore the DVLS API and understand the flow we’ll need to implement in our action. This exploration phase is crucial when creating any custom action – you need to understand the API requirements before implementing them.

$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

This exploration reveals the API flow we’ll need to implement in our GitHub Action:

  1. Authenticate with DVLS using app credentials
  2. Get the vault information using the returned token
  3. Locate the specific entry ID for our secret
  4. Retrieve the actual secret value

Understanding this flow is crucial because we’ll need to implement the same steps in our GitHub Action, just using JavaScript instead of PowerShell.

When creating your own custom action, you’ll follow a similar process:

  1. Identify the API endpoints you need to interact with
  2. Test the authentication and data retrieval process
  3. Document the steps you’ll need to implement in your action

Step 2: Creating the Authentication Action

Now that we understand the API flow, let’s create our first custom action for handling authentication. We’ll build this in a new shared repository.

Setting Up the Action Structure

First, create the following file structure in your repository:

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

This file structure is organized to create a modular and maintainable GitHub Action:

  • login/ – A dedicated directory for the authentication action, keeping related files together
  • index.js – The main action code that contains the authentication logic and API interactions
  • action.yml – Defines the action’s interface, including required inputs and how to run the action
  • package.json – Manages dependencies and project metadata
  • README.md – Documentation for users of the action

This structure follows best practices for GitHub Actions, keeping the code organized and making it easy to maintain and update the action over time.

Creating the Action Code

First, you must create the action code. This involves creating the main JavaScript file that will handle the authentication logic:

  1. Create index.js – this is where the main action logic lives:
// 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();

The code uses the @actions/core package from GitHub’s toolkit to handle inputs, outputs, and logging. We’ve also implemented robust error handling and logging to make debugging easier.

Don’t worry too much about understanding all the JavaScript code details here! The key point is that this GitHub Action code just needs to do one main thing: use core.setOutput() to return the authentication token.

If you’re not comfortable writing this JavaScript yourself, you can use tools like ChatGPT to help generate the code. The most important part is understanding that the action needs to:

  • Get the input values (like server URL and credentials)
  • Make the authentication request
  • Return the token using core.setOutput()

Creating the NodeJS Package

Now that we understand the code structure and functionality of our action, let’s set up the Node.js package configuration. This involves creating the necessary package files and installing dependencies that our action will need to function properly.

  1. Create package.json to define our dependencies and other action metadata.
    {
        "name": "devolutions-server-login",
        "version": "1.0.0",
        "description": "GitHub Action to authenticate to 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. Install dependencies by running npm install.
    npm install
    

    After installing dependencies, you should see a new node_modules directory created in your project folder. This directory contains all the required packages that your action needs to run.

    Note: While we’ll commit package.json and package-lock.json to version control, we’ll eventually exclude the node_modules directory by using ncc to bundle our dependencies.

  3. Create action.yml to define the action’s interface:
    name: 'Devolutions Server Login'
    description: 'Authenticate and get a token from Devolutions Server'
    inputs:
      server_url:
        description: 'URL of the Devolutions Server'
        required: true
      app_key:
        description: 'Application key for authentication'
        required: true
      app_secret:
        description: 'Application secret for authentication'
        required: true
      output_variable:
        description: 'Name of the environment variable to store the retrieved token'
        required: false
        default: 'DVLS_TOKEN'
    runs:
      using: 'node20'
      main: 'index.js'
    

    The action.yml file is crucial as it defines how your action will work within GitHub Actions workflows. Let’s break down its key components:

    • name and description: These provide basic information about what your action does
    • inputs: Defines the parameters that users can pass to your action:
      • server_url: Where to find the Devolutions Server
      • app_key and app_secret: Authentication credentials
      • output_variable: Where to store the resulting token
    • runs: Specifies how to execute the action:
      • using: 'node20': Uses Node.js version 20
      • main: 'index.js': Points to the main JavaScript file

    When users reference this action in their workflows, they’ll provide these inputs according to this interface definition.

Optimizing the Action

To make our action more maintainable and efficient, we’ll use Vercel’s ncc compiler to bundle all dependencies into a single file. This eliminates the need to commit the node_modules directory:

Including node_modules in your GitHub Action repository is not recommended for several reasons:

  • The node_modules directory can be very large, containing all dependencies and their sub-dependencies, which would bloat the repository size unnecessarily
  • Different operating systems and environments may handle node_modules differently, potentially causing compatibility issues
  • Using Vercel’s ncc compiler to bundle all dependencies into a single file is a better approach because it:
    • Creates a more efficient and maintainable action
    • Eliminates the need to commit the node_modules directory
  1. Install ncc:
    npm i -g @vercel/ncc
    
  2. Build the bundled version:
    ncc build index.js --license licenses.txt
    
  3. Update action.yml to point to the bundled file:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Updated to use the bundled version
    
  4. Clean up:
    rm -rf node_modules  # Remove node_modules directory
    
  5. Commit the files to the shared repo.
    git add .
    git commit -m "Initial commit of DVLS login action"
    git push
    

Creating the README

Everyone loves documentation, right? No? Well, I don’t either so I’ve created a README template for you to use. Be sure to fill this out and include it with your 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:

steps:

  • name: Prerequisite Step
    uses: example/action-name@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: Step Name
    uses: your-org/action-name@v1
    with:
    inputname: ‘Input Value’
    another
    input: ‘Another Value’
## Example Workflow

Here's a complete example workflow utilizing this action:

name: Example Workflow
on: [push]

jobs:
example-job:
runs-on: ubuntu-latest
steps:
– name: Checkout Repository
uses: 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).

Key Points to Remember

When creating your own custom action:

  1. Always implement thorough error handling and logging
  2. Use the @actions/core package for proper GitHub Actions integration
  3. Bundle dependencies with ncc to keep the repository clean
  4. Document inputs and outputs clearly in your action.yml
  5. Consider security implications and mask sensitive values using core.setSecret()

This authentication action will be used by our next action that retrieves secrets. Let’s move on to creating that action.

Step 3: Creating the “Get Secret” Action

You’ve done the hard work up to this point. You now know how to create a custom Github action. If you’re following along, you now need to repeat those steps for the DVLS get secret entry action as follows:

The Action Structure

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

The index.js File

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

Optimizing the Action

  1. Compile the index file.
    npm i -g @vercel/ncc
    ncc build index.js --license licenses.txt
    
  2. Update action.yml to point to the bundled file:
    runs:
      using: 'node20'
      main: 'dist/index.js'  # Updated to use the bundled version
    
  3. Clean up:
    rm -rf node_modules  # Remove node_modules directory
    
  4. Commit the files to the shared repo.
    git add .
    git commit -m "Initial commit of DVLS get secret entry action"
    git push
    

The End Result

At this point, you should have two GitHub repos:

  • the repo containing the workflow you had using GitHub secrets
  • the shared repo (assuming the name is dvls-actions) containing the two actions with a structure looking like this:
    dvls-actions/
    ├── login/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    ├── get-secret-entry/
    │   ├── index.js
    │   ├── action.yml
    │   ├── package.json
    │   └── README.md
    

Using the Custom Actions

Once you’ve set up these custom actions, you can use them in your original calling workflow.

Original workflow:

  • Uses a single step to send a Slack notification
  • Directly references the webhook URL from secrets (secrets.SLACK_WEBHOOK_URL)

New workflow:

  • Adds authentication step using the custom DVLS login action
  • Retrieves the Slack webhook URL securely from Devolutions Server
  • Uses environment variables instead of secrets
  • Maintains the same notification functionality but with enhanced security

The new workflow adds two steps before the Slack notification:

  1. Authentication with Devolutions Server using the dvls-login action
  2. Retrieval of the Slack webhook URL using the dvls-get-secret-entry action
  3. The final Slack notification step remains similar but uses the retrieved webhook URL from an environment variable (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:"
          }'

Creating custom GitHub Actions allows you to standardize and secure your workflows across multiple repositories. By moving sensitive operations like authentication and secret retrieval into dedicated actions, you can:

  • Maintain better security practices by centralizing credential management
  • Reduce code duplication across different workflows
  • Simplify workflow maintenance and updates
  • Ensure consistent implementation of critical operations

The example of integrating Devolutions Server with GitHub Actions demonstrates how custom actions can bridge the gap between different tools while maintaining security best practices. This approach can be adapted for various other integrations and use cases in your DevOps workflows.

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!