GitHub JavaScript & Docker Actions Explained | Hands-on Demos
This chapter completes the deep dive into GitHub Custom Actions by examining JavaScript Actions and Docker Actions. It covers the architecture, runtime environments, dependency management, and ideal use cases for each action type. Through hands-on implementations, you will learn how to encapsulate reusable pipeline logic using JavaScript/TypeScript and how to package custom environments using Docker containers.
1. Evolution of Custom Actions: Moving Beyond Composite Workflows
In previous modules, custom actions were established as the standard mechanism for encapsulating reusable workflow logic across an enterprise organization. Custom actions eliminate duplicate code across individual pipeline definitions (.github/workflows/*.yml) by standardizing organizational policies, build processes, and security checks into single reusable modules.
While Composite Actions allow for the chaining of multiple workflow steps and shell scripts, complex pipeline operations quickly hit the limits of shell scripting.
To support advanced automation—such as interacting with REST APIs, handling complex data formats, or managing intricate control flows—GitHub Actions provides two higher-level action types:
- JavaScript Actions: Logic written in JavaScript or TypeScript that runs directly on the runner host via a Node.js runtime environment.
- Docker Actions: Logic packaged alongside its OS dependencies, tools, and runtime inside a Docker container image that executes on top of the runner host.
2. Deep Dive: JavaScript Custom Actions
JavaScript actions implement custom automation logic using JavaScript or its statically typed superset, TypeScript.
Note: TypeScript is a strict syntactical superset of JavaScript, meaning that any valid JavaScript program is also a valid TypeScript program. TypeScript code compiles down to plain JavaScript before execution.
Core Architectural Characteristics
- Node.js Execution Environment: JavaScript actions run using the Node.js runtime engine provided natively on GitHub-hosted runners (such as
ubuntu-latest). - Native GitHub API Integration: They integrate seamlessly with the GitHub API, octokit, and the official
@actions/*toolkit libraries. - Marketplace Dominance: Approximately 95% to 97% of all published actions on the GitHub Actions Marketplace—including official actions like
actions/checkout,actions/setup-node,actions/cache, andactions/upload-artifact—are authored as JavaScript actions.
3. Real-World Analogy & Trade-offs: Scripting vs. Programming Languages
Real-Life Analogy: The Banking Application Calculator
Imagine you are building a modern mobile banking application. When you need to calculate interest rates or monthly loan payments, you do not write fundamental mathematical algorithms or build a digital calculator from scratch. Instead, you import a vetted, highly tested external financial calculation library. This frees you to concentrate entirely on your core business logic.
Shell scripting often forces DevOps engineers to reinvent the wheel for data parsing, string manipulating, or HTTP retries. A full programming language like JavaScript provides access to a massive library ecosystem (npm), structured modules, robust error handling (
try/catch), and typed data structures.
Scripting (Composite) vs. Programming (JavaScript)
Team Competency Decision Framework
Despite the power of JavaScript, team competency is the ultimate decision factor:
- If your DevOps team lacks JavaScript expertise: Sticking with shell-based Composite Actions is often better in the long run. Maintainability by the primary operational team outweighs theoretical feature advantages.
- If your team has JavaScript/TypeScript expertise: JavaScript Actions provide a cleaner, more reliable, and faster execution model for complex automation tasks.
4. JavaScript Action Architecture & Directory Structure
JavaScript custom actions can be stored locally within the application repository or centralized in a dedicated remote repository for organization-wide use.
File Organization (Local Action Pattern)
To establish a local JavaScript action within a project repository, follow this standardized directory layout:
Purpose of Key Files
action.yml: The metadata manifest required for all custom actions. Defines inputs, outputs, runtime specifications (using: 'node20'or'node24'), and the main script path.index.js: Contains the executable JavaScript logic.package.json: Standard Node.js manifest defining external package dependencies, such as@actions/coreand@actions/github.
5. Hands-on Walkthrough: Building a JavaScript Custom Action
In this demonstration, we create a JavaScript Action (workflow-summary) that accepts user inputs, logs contextual metadata about the current workflow run, and handles runtime exceptions.
Step 1: Define Dependencies (package.json)
{
"name": "workflow-summary",
"version": "1.0.0",
"description": "Generates workflow contextual summary using GitHub Actions Toolkit",
"main": "index.js",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0"
}
}
Step 2: Implement Execution Logic (index.js)
const core = require('@actions/core');
const github = require('@actions/github');
try {
// 1. Retrieve inputs defined in action.yml
const nameToGreet = core.getInput('name', { required: true });
// 2. Log informative messages to the workflow console
core.info(`GitHub JavaScript Action Execution Started.`);
core.info(`Hello, ${nameToGreet}!`);
// 3. Extract metadata from the GitHub Context object
core.info('--- Workflow Execution Context ---');
core.info(`Workflow Name : ${github.context.workflow}`);
core.info(`Action Event : ${github.context.eventName}`);
core.info(`Ref Branch : ${github.context.ref}`);
core.info(`Run Number : ${github.context.runNumber}`);
core.info(`Commit SHA : ${github.context.sha}`);
core.info(`Actor : ${github.context.actor}`);
} catch (error) {
// Gracefully handle exceptions and set workflow step failure state
core.setFailed(`Action failed with error: ${error.message}`);
}
Step 3: Configure Metadata Manifest (action.yml)
name: 'Workflow Summary'
description: 'Logs contextual execution details using Node.js'
inputs:
name:
description: 'Name of the user or platform trigger'
required: true
default: 'Developer'
runs:
using: 'node20' # Specifies execution via Node.js runtime engine
main: 'index.js' # Specifies entrypoint execution file
Note on Dist Bundling: In production marketplace actions, developers typically bundle dependency code into a single file (e.g.,
dist/index.jsusing tools like@vercel/ncc) so consumers don't need to runnpm installduring execution. For this local action demonstration, we install dependencies explicitly on the runner.
Step 4: Create Calling Workflow (.github/workflows/javascript-action-demo.yml)
name: JavaScript Action Demo
on:
workflow_dispatch:
jobs:
demo-job:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository Source
uses: actions/checkout@v4
- name: Install Action Dependencies
run: |
cd .github/actions/workflow-summary
npm install
- name: Execute Local JavaScript Action
uses: ./.github/actions/workflow-summary
with:
name: 'CloudWithVarun'
6. Deep Dive: Docker Custom Actions
Docker Actions allow you to package your workflow logic alongside its operational execution environment, dependencies, system binaries, and tools inside a Docker image.
Core Architectural Characteristics
- Isolated Execution Environment: Runs inside a dedicated Docker container created dynamically on the runner VM during step execution.
- Language Agnostic: Unlocks complete freedom regarding programming languages. You can author your action logic in Python, Go, Java, Rust, Shell, or C#, provided the runtime is installed in the container image.
- Consistent Operations: Guarantees identical execution regardless of updates to the underlying GitHub-hosted runner OS environment.
7. Docker Action Architecture & Execution Model
Directory Structure (Local Docker Action)
File Responsibilities
Dockerfile: Defines the base image, installs system binaries/packages, copies the execution script into the image filesystem, and sets theENTRYPOINT.entrypoint.sh: The script or executable binary triggered when the container starts. Accepts input parameters passed as CLI arguments fromaction.yml.action.yml: Configures the action to run usingdocker, references theDockerfile, and passes input values into theargsarray.
8. Hands-on Walkthrough: Building a Docker Custom Action
In this demonstration, we build a Docker Action (docker-system-info) using a lightweight Alpine Linux image. The action accepts an input parameter, outputs greeting logs, inspects the container's release metadata, and displays system diagnostics.
Step 1: Create Container Entrypoint (entrypoint.sh)
#!/bin/sh
set -e
# Capture the first CLI argument passed from action.yml
NAME=$1
echo "=================================================="
echo " GITHUB DOCKER ACTION EXECUTION "
echo "=================================================="
echo "Hello, ${NAME}!"
echo ""
echo "--- Container Operational Diagnostics ---"
echo "Operating System Information:"
cat /etc/os-release
echo ""
echo "Current User ID : $(whoami)"
echo "Working Directory : $(pwd)"
echo "=================================================="
Step 2: Build Container Specification (Dockerfile)
# Use lightweight Alpine Base Image
FROM alpine:3.19
# Copy entrypoint script to container root
COPY entrypoint.sh /entrypoint.sh
# Ensure execution permissions on entrypoint script
RUN chmod +x /entrypoint.sh
# Set entrypoint binary trigger
ENTRYPOINT ["/entrypoint.sh"]
Step 3: Configure Metadata Manifest (action.yml)
name: 'Docker System Diagnostics'
description: 'Executes system diagnostic logic inside a custom Docker container'
inputs:
name:
description: 'Name parameter passed to container entrypoint'
required: true
default: 'DevOps Engineer'
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.name }}
Note: The
argslist inaction.ymlmaps directly to positional parameters ($1,$2, etc.) in the container'sENTRYPOINTexecutable shell script.
Step 4: Create Calling Workflow (.github/workflows/docker-action-demo.yml)
name: Docker Action Demo
on:
workflow_dispatch:
jobs:
demo-job:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Execute Local Docker Action
uses: ./.github/actions/docker-system-info
with:
name: 'CloudWithVarun'
9. Structural Comparison & Selection Framework
Choosing the right custom action type depends on your execution requirements, performance targets, ecosystem dependencies, and team skill set.
Comprehensive Action Type Comparison
Decision Flowchart
10. Summary and Key Takeaways
- Custom Actions Value Proposition: Packaging pipeline logic into custom actions reduces code duplication across workflow manifests, centralizes organizational policies, and simplifies pipeline maintenance.
- JavaScript Action Power: Executing via Node.js natively on the host runner, JavaScript actions provide fast execution and access to the vast
npmecosystem alongside GitHub's@actions/*toolkit. - Marketplace Dominance: Over 95% of marketplace actions (
checkout,cache,setup-node) are written in JavaScript due to their speed, platform compatibility, and API capabilities. - Docker Action Versatility: Docker actions package automation logic alongside its execution environment, enabling developers to write pipelines in any language (Python, Go, Java) with explicit OS dependencies.
- Container Isolation Trade-off: Docker actions guarantee consistent environment execution across hosts, but incur performance overhead during job runs due to image pulling and container creation steps.
- Positional Argument Mapping: Docker actions map
inputsfromaction.ymlinto CLI positional arguments ($1,$2) inside the containerENTRYPOINTexecution context. - Pragmatic Selection Strategy: Match your custom action architecture to team skill sets—prefer JavaScript for API-heavy Node workflows, Docker for specialized binaries and non-JS languages, and Composite actions for basic shell scripting.