Table of Contents
Open Table of Contents
- Introduction
- Why Create Custom Actions?
- Setting Up Your Development Environment
- Creating the Action Metadata File and README
- Building Your First JavaScript Action
- Working with the
@actions/core
Package - Interacting with the GitHub API using
@actions/github
- Putting It All Together: An Advanced Example
- Usage in a Workflow
- Conclusion
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:
- Encapsulate reusable logic specific to your workflows.
- Share functionality across multiple repositories or with the community.
- Integrate with external APIs or services in a way that’s tailored to your requirements.
Docker vs. JavaScript Actions
You can develop custom GitHub Actions using:
- Docker containers
- JavaScript
- Composite actions (combining multiple actions into one)
Let’s focus on Docker and JavaScript actions.
Docker Actions
Pros:
- Highly flexible and can encapsulate any environment or dependencies.
- Self-contained, ensuring consistency across runs.
Cons:
- Require building or pulling the Docker image on each workflow run, which can slow down execution.
- Only run on Linux-based runners.
JavaScript Actions
Pros:
- Execute directly on the runner machine—faster execution.
- Cross-platform compatibility (Linux, macOS, Windows).
- Bundled with dependencies, making them portable.
Cons:
- Limited to Node.js environments.
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.
- 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:
name
: The display name for your action.description
: A short description.inputs
: Define any inputs your action requires.runs
: Specifies that the action uses Node.js (node16) and points to the compiled JavaScript file (dist/index.js).
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:
- Notice: Informational messages.
- Warning: Highlights potential issues.
- Error: Indicates failures but doesn’t stop the workflow unless specified.
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: