Skip to content

Building Custom GitHub Actions with JavaScript

Updated: at 11:12 AM (6 min read)

Table of Contents

Open Table of Contents

Introduction

GitHub Actions revolutionizes how developers automate, extend, and run custom software development life cycles within GitHub. While the GitHub Marketplace already contains thousands of prebuilt actions, there are those times when you will need something that suits your needs. This is where custom actions come in.

In this tutorial, we will show how to create GitHub Actions with JavaScript. We are going to speak about advantages of using JavaScript actions in comparison with other types, setup the development environment, and create a simple action with advanced features using the @actions/core and @actions/github packages.

Why Create Custom Actions?

While the GitHub Marketplace provides a vast array of actions for various tasks—setting up environments, building, testing, and deploying applications—you might encounter scenarios where existing actions don’t quite fit your needs. Custom actions allow you to:

Docker vs. JavaScript Actions

You can develop custom GitHub Actions using:

Let’s focus on Docker and JavaScript actions.

Docker Actions

Pros:

Cons:

JavaScript Actions

Pros:

Cons:

Setting Up Your Development Environment

Before diving into code, ensure you have the following set up:

Create a Dedicated Repository

Having a dedicated repository for each custom action allows better versioning control and easier reuse.

  1. Initialize a new repository on GitHub and clone it to your local machine.

Set Up .gitignore

Use the Node.js .gitignore template to avoid committing unnecessary files.

Include:

node_modules/
dist/

Install Node.js and npm

Download and install from nodejs.org or use your package manager.

Verify installation:

node -v
npm -v

Initialize the Project

Navigate to your action’s directory and run:

npm init -y

Install Dependencies

Install the necessary packages:

npm install @actions/core @actions/github

Install ncc globally to compile your action:

npm install -g @vercel/ncc

Creating the Action Metadata File and README

action.yml

This file defines the metadata for your action and must be located in the root of your repository.

# action.yml

name: "My Custom Action"
description: "A brief description of what your action does"
author: "Your Name"
inputs:
  input_1:
    description: "An example input"
    required: false
    default: "World"
runs:
  using: "node16"
  main: "dist/index.js"

Key Points:

README.md

A well-documented README is crucial, especially if you plan to share your action.

# My Custom Action

This action prints "Hello World" or "Hello" + the name of a person to greet.

## Inputs

### `input_1`

**Required**: No  
**Default**: "World"

The name of the person to greet.

## Example usage

```yaml
uses: yourusername/your-repo-name@v1
with:
  input_1: "Alice"
```

### Tweaking `.gitignore` and `package.json`

#### Updating `.gitignore`

By default, the `dist` directory might be ignored. Since we need to commit the compiled action, ensure the `dist` directory is **not** ignored.

```diff
# .gitignore

node_modules/
- dist/

Updating package.json

Add build and test scripts to streamline development.

{
  "name": "your-action-name",
  "version": "1.0.0",
  "description": "Your action description",
  "main": "index.js",
  "scripts": {
    "build": "ncc build index.js -o dist",
    "test": "node dist/index.js"
  },
  "dependencies": {
    "@actions/core": "^1.10.0",
    "@actions/github": "^5.1.1"
  },
  "devDependencies": {
    "@vercel/ncc": "^0.34.0"
  }
}

Building Your First JavaScript Action

Let’s start with a simple action that logs “Hello, [input]”.

index.js

// index.js
const core = require("@actions/core");

try {
  const nameToGreet = core.getInput("input_1");
  console.log(`Hello, ${nameToGreet}!`);
  core.setOutput("greeting", `Hello, ${nameToGreet}!`);
} catch (error) {
  core.setFailed(error.message);
}

Build the Action

Run the build script to compile your code and dependencies into a single file in the dist directory.

npm run build

Working with the @actions/core Package

The @actions/core package provides essential functions for your action to interact with the workflow.

Handling Inputs and Outputs

Define inputs in action.yml and access them in your code.

action.yml

inputs:
  name:
    description: "Name to greet"
    required: true

index.js

const core = require("@actions/core");

try {
  const name = core.getInput("name");
  core.setOutput("greeting", `Hello, ${name}!`);
} catch (error) {
  core.setFailed(error.message);
}

Creating Annotations

Use annotations to highlight messages in the workflow logs.

core.notice("This is a notice");
core.warning("This is a warning");
core.error("This is an error");

Workflow Log Appearance:

Setting Exit Codes

To fail the action and stop the workflow:

core.setFailed("Action has failed!");

Interacting with the GitHub API using @actions/github

The @actions/github package allows your action to interact with GitHub’s REST API.

Accessing the Context

The context contains information about the workflow run and event that triggered it.

const github = require("@actions/github");

const context = github.context;
console.log(`Event: ${context.eventName}`);
console.log(`Action: ${context.action}`);
console.log(`Workflow: ${context.workflow}`);

Using the Octokit Client

Create an authenticated client to make API calls.

const core = require("@actions/core");
const github = require("@actions/github");

const token = core.getInput("github_token");
const octokit = github.getOctokit(token);

Example: Commenting on a Pull Request

if (context.eventName === "pull_request") {
  const prNumber = context.payload.pull_request.number;
  await octokit.rest.issues.createComment({
    ...context.repo,
    issue_number: prNumber,
    body: "Thank you for your pull request!",
  });
}

Putting It All Together: An Advanced Example

Let’s build an action that adds a comment and label to new pull requests.

action.yml

name: "PR Welcome Action"
description: "Comments on new PRs and adds a label"
inputs:
  github_token:
    description: "GitHub token"
    required: true
runs:
  using: "node16"
  main: "dist/index.js"

index.js

const core = require("@actions/core");
const github = require("@actions/github");

async function run() {
  try {
    const token = core.getInput("github_token");
    const octokit = github.getOctokit(token);
    const { context } = github;

    if (context.eventName !== "pull_request") {
      core.warning("This action only runs on pull requests.");
      return;
    }

    const pr = context.payload.pull_request;

    if (pr.state !== "open" || context.payload.action !== "opened") {
      core.info("Pull request is not newly opened. Exiting.");
      return;
    }

    // Add a comment
    await octokit.rest.issues.createComment({
      ...context.repo,
      issue_number: pr.number,
      body: "Thank you for your contribution! 🎉",
    });

    // Add a label
    await octokit.rest.issues.addLabels({
      ...context.repo,
      issue_number: pr.number,
      labels: ["welcome"],
    });

    core.info("Commented and labeled the PR.");
  } catch (error) {
    core.setFailed(error.message);
  }
}

run();

Build the Action

npm run build

Usage in a Workflow

# .github/workflows/pr-welcome.yml
name: "PR Welcome"
on:
  pull_request:
    types: [opened]

jobs:
  welcome:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Run PR Welcome Action
        uses: yourusername/your-action-repo@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

Conclusion

Writing GitHub Actions in JavaScript allows you to automate and customize your workflows around needs that may not be satisfiable by existing actions. Using the @actions/core and @actions/github package, one can deeply interact with the GitHub Actions framework as well as GitHub’s API for unlimited possibilities.

Key Takeaways:

JavaScript actions are faster and more portable than Docker actions. Action metadata is responsible for defining inputs, outputs, and how your action will execute. The package @actions/core provides helper functions for inputs, outputs, annotations, and exit code. The package @actions/github gives you access to context and to download/upload files, or use any other GitHub API using the authenticated Octokit client. Testing and debugging has to be done with console.log and with annotations, checking the workflow logs when necessary.

Further Resources: