GitHub Actions is a native CI/CD system. In this tutorial, we going to cover how to create a private Github Action that deploys a binary released as part of another private repository.
The usage of actions from a private repositories differs from a regular action in a public repo in 2 points:
- Need to git checkout the action code from the private repo, which requires a decidate Personal Access Token (PAT) with
repo
permissions.GITHUB_TOKEN
is not enough since it only has permissions to the current repo where the workflow is being executed - Run the action using the local path where it was checked out instead of repository url.
Implementation
A Github action (javascript type) needs to declare at least the 2 following files:
action.yml
: Contains the metadata for the action (e.g: description, input and output parameters, the.js
file to be run, etc)- The
.js
to be invoked (needs to match with the value inaction.yml
). Usuallyindex.js
ormain.js
Tips to understand the code blocks below:
- Anything between angular brackets (e.g
<owner>
,<repo>
and<tool>
) should be replaced by actual values if the code where to be used PRIVATE_REPO_ACCESS_TOKEN
needs to be declared as a secret in the github repo where the workflow invokes the private action
action.yml
The action.yml
below declares the 2 input parameters required by this action:
version
(optional): Value to determine which tool version to install. If not specified, defaults tolatest
token
(required): PAT to authenticate against the private repository where the
name: "Github Action for tool"
description: "tool description"
author: "author name"
branding:
icon: "droplet"
color: "blue"
inputs:
version:
description: "Version of tool to install"
default: "latest"
required: false
token:
description: "PAT with permission to download from private repos"
default: ""
required: true
runs:
using: "node16"
main: "index.js"
index.js script
The index.js
contains the logic of the action to execute. Its implementation can be divided in 3 components:
- Resolve the version of the tool to install using
octokit/repo.js
library. - Download the tool from the private repo using
@actions/tool-cache
library. - Make the tool available locally in path using
@actions/core
and@actions/exec
libraries.
Want to directly to the complete index.js
script? Go here
Resolve version of tool to install
The resolveReleaseData
function takes care of obtaining the release information through the GitHub API for the version to be installed (a specific version or the latest one). For that, it leverages the octokit/repo.js
library as follows:
- If no version or version parameter is set to
latest
, invokegetLatestRelease
function to get the data about the latest release - If version is set to something other than
latest
, use thegetReleaseByTag
function to find the information about that particular release.
To execute the above functions, the parameters needs to include:
owner
andrepo
to establish from which repo to get the data.headers
containing theauthorization
field with the PAT token.
async function resolveReleaseData() {
let version = core.getInput('version');
let token = core.getInput('token');
let releaseData = null
let params = {
owner: '<owner>',
repo: '<repo>',
headers: {
accept: 'application/vnd.github.v3+json',
authorization: `token ${token}`
}
}
if ((!version) || (version.toLowerCase() === 'latest')) {
core.info("Get release info for latest version")
releaseData = await octokit.repos.getLatestRelease(params).then(result => {
return result.data;
})
} else {
core.info(`Get release info for release ${version}`)
params["tag"] = version
releaseData = await octokit.repos.getReleaseByTag(params).then(result => {
return result.data;
})
}
return releaseData
}
Download tool
Using the release information obtained from the resolveReleaseData
, the downloadTool
function takes care of downloading the correct binary according to the platform version where it is being installed (linux
or darwin
).
- In this example, all the tools version in a given release are named following the pattern:
<tool>_<platform>_amd64
. tc.downloadTool
takes care of downloading the particular binary from the release, using the url associated with the asset. Since it is a private project, it requires the PAT to authenticate and the headeraccept: 'application/octet-stream'
.
async function downloadTool(releaseData) {
core.info(`Downloading <tool> from ${releaseData.html_url}`)
let token = core.getInput('token');
let platform = 'linux';
if (process.platform === 'darwin') {
platform = 'darwin';
}
let assetName = `<tool>_${platform}_amd64`
let asset = releaseData.assets.find(obj => {
return obj.name == assetName
})
const toolDownload = await tc.downloadTool(
asset.url,
undefined,
`token ${token}`,
{
accept: 'application/octet-stream'
}
);
return toolDownload;
}
Making the tool available in path
After downloading the binary, using downloadTool
function, it is recommend to make it available on path, so that users can invoke it without having to specify the full path to the binary.
tc.cacheFile
copies the file to the caching directory and can rename the binary (which the code below does). That allows users to use the tool astool ...
instead of/path/to/tool ..
.exec.exec
runs thechmod
command to make sure that the binary is executablecore.addPath
adds the path to the binary to the$PATH
environment variable to allow binary usage without having to specify the full path.
async function makeAvailableInPath(download, version) {
let name = '<tool>'
core.info(`Cache file ${download} and rename to generic name`);
const cachedPath = await tc.cacheFile(download, name, name, version);
const filePath = path.join(cachedPath, name)
core.info(`Making <tool> binary executable`);
await exec.exec("chmod", ["+x", filePath]);
core.info(`Make ${cachedPath} available in path`);
core.addPath(cachedPath);
}
Complete index.js
The complete index.js file combining the code blocks from the 3 previous sections, plus the run
function which orchestrate the functions usage.
const core = require('@actions/core');
const exec = require('@actions/exec');
const tc = require('@actions/tool-cache');
const { Octokit } = require("@octokit/rest");
const path = require("path");
const octokit = new Octokit();
async function resolveReleaseData() {
let version = core.getInput('version');
let token = core.getInput('token');
let releaseData = null
let params = {
owner: '<owner>',
repo: '<repo>',
headers: {
accept: 'application/vnd.github.v3+json',
authorization: `token ${token}`
}
}
if ((!version) || (version.toLowerCase() === 'latest')) {
core.info("Get release info for latest version")
releaseData = await octokit.repos.getLatestRelease(params).then(result => {
return result.data;
})
} else {
core.info(`Get release info for release ${version}`)
params["tag"] = version
releaseData = await octokit.repos.getReleaseByTag(params).then(result => {
return result.data;
})
}
return releaseData
}
async function downloadTool(releaseData) {
core.info(`Downloading <tool> from ${releaseData.html_url}`)
let token = core.getInput('token');
let platform = 'linux';
if (process.platform === 'darwin') {
platform = 'darwin';
}
let assetName = `<tool>_${platform}_amd64`
let asset = releaseData.assets.find(obj => {
return obj.name == assetName
})
const toolDownload = await tc.downloadTool(
asset.url,
undefined,
`token ${token}`,
{
accept: 'application/octet-stream'
}
);
return toolDownload;
}
async function makeAvailableInPath(download, version) {
let name = '<tool>'
core.info(`Cache file ${download} and rename to generic name`);
const cachedPath = await tc.cacheFile(download, name, name, version);
const filePath = path.join(cachedPath, name)
core.info(`Making <tool> binary executable`);
await exec.exec("chmod", ["+x", filePath]);
core.info(`Make ${cachedPath} available in path`);
core.addPath(cachedPath);
}
async function run() {
try {
let releaseData = await resolveReleaseData()
let version = releaseData.tag_name
core.info(`>>> Version to set up: ${version}`);
let path = tc.find("<tool>", version);
if (!path) {
let download = await downloadTool(releaseData)
await makeAvailableInPath(download, version);
core.info(`>>> <tool> version ${version} installed successfully`);
} else {
core.info(`>> <tool> version ${version} already installed`)
}
await exec.exec('<tool> --help');
core.info('>>> Successfully invoked <tool> help');
}
catch (error) {
core.setFailed(error.message);
}
}
run();
Action usage
Since Github Actions doesn’t support invoking actions from private repos directly, we need the following workaround to use the action (named setup-tool
in this example) in our workflows:
- First checkout the private repo that defines the
setup-tool
action into your current repo, using a token withrepo
permissions (secrets.PRIVATE_REPO_ACCESS_TOKEN
in the example below) - Run the
setup-tool
action from the local path, which is a supported usage
- uses: actions/checkout@v2
with:
repository: <owner>/setup-tool
token: ${{ secrets.PRIVATE_REPO_ACCESS_TOKEN }}
persist-credentials: false
path: ./.github/actions/setup-tool
- uses: ./.github/actions/setup-tool
with:
token: ${{ secrets.PRIVATE_REPO_ACCESS_TOKEN }}
Notes
persist-credentials: false
avoids saving the access token into the current git configuration. This means that subsequent git operations will still rely on the default GitHub Action setup (able to pull the current project throughGITHUB_TOKEN
and only public repos in addition)
Future steps
- Make the action generic so that it can install a published binary tool from any repository (public or private)