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:
- Connect to the external service
- Authenticate
- Perform specific operations
- 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:
- Create a public GitHub repository for shared actions
- Build multiple interconnected actions (we’ll create two as examples):
- One to handle authentication
- Another to perform specific operations
- 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:
- Prepare DVLS Environment
- Create corresponding secrets in DVLS
- Test DVLS API endpoints for authentication and secret retrieval
- 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
- Build an action for DVLS authentication (
- 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 authenticationDVLS_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:
- Authenticate with DVLS using app credentials
- Get the vault information using the returned token
- Locate the specific entry ID for our secret
- 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:
- Identify the API endpoints you need to interact with
- Test the authentication and data retrieval process
- 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:
- 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.
- 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" } }
- 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
andpackage-lock.json
to version control, we’ll eventually exclude thenode_modules
directory by usingncc
to bundle our dependencies. - 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 Serverapp_key
andapp_secret
: Authentication credentialsoutput_variable
: Where to store the resulting token
- runs: Specifies how to execute the action:
using: 'node20'
: Uses Node.js version 20main: '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
- Install
ncc
:npm i -g @vercel/ncc
- Build the bundled version:
ncc build index.js --license licenses.txt
- Update
action.yml
to point to the bundled file:runs: using: 'node20' main: 'dist/index.js' # Updated to use the bundled version
- Clean up:
rm -rf node_modules # Remove node_modules directory
- 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’
anotherinput: ‘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:
- Always implement thorough error handling and logging
- Use the
@actions/core
package for proper GitHub Actions integration - Bundle dependencies with
ncc
to keep the repository clean - Document inputs and outputs clearly in your
action.yml
- 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
- Compile the index file.
npm i -g @vercel/ncc ncc build index.js --license licenses.txt
- Update
action.yml
to point to the bundled file:runs: using: 'node20' main: 'dist/index.js' # Updated to use the bundled version
- Clean up:
rm -rf node_modules # Remove node_modules directory
- 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:
- Authentication with Devolutions Server using the
dvls-login
action - Retrieval of the Slack webhook URL using the
dvls-get-secret-entry
action - 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.