While developing Mass Action Scheduler, I came up with my own custom GitHub labels to help me better organize issues and feature requests, as well as be a way to better communicate to users on the status of their feedback.

Of all the labels for Mass Action Scheduler, there’s at least 12 of them that I would like to reuse in my other projects. As of July 2019, GitHub does not provide an “easy” way to copy labels from one repository to another. However, after searching the interwebs I did eventually discover multiple ways to do it with varying levels of effort. I thought it’d be nice to share the different approaches I came across and which I ended up using.

My Favorite Solutions

My two favorite solutions are (1) my own approach using the command line and the GitHub API, and (2) some JavaScript functions that you run in your browser’s console to automate clicks on GitHub.com.

Command Line Automation with Bash

The solution I ultimately went with was a ~50 line bash script that uses curl, jq, and the GitHub API. What I liked about my custom script is that I could have it do exactly what I wanted, haha. It supports copying the label’s name, color, and description; and will create or update labels in the destination repo. The other solutions I explored would not overwrite labels in the destination org. I don’t blame them, that’s probably a safe choice. However, for my purposes I did want to update labels so that I can keep the colors and descriptions in sync across the projects if and when the labels change. Because this uses the GitHub API, you do need to provide a personal access token.

# This script uses the GitHub Labels REST API
# https://developer.github.com/v3/issues/labels/
# Provide a personal access token that can
# access the source and target repositories.
# This is how you authorize with the GitHub API.
# https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line
# If you use GitHub Enterprise, change this to "https://<your_domain>/api/v3&quot;
# The source repository whose labels to copy.
# The target repository to add or update labels.
# ---------------------------------------------------------
# Headers used in curl commands
GH_ACCEPT_HEADER="Accept: application/vnd.github.symmetra-preview+json"
GH_AUTH_HEADER="Authorization: Bearer $GH_TOKEN"
# Bash for-loop over JSON array with jq
# https://starkandwayne.com/blog/bash-for-loop-over-json-array-using-jq/
sourceLabelsJson64=$(curl --silent -H "$GH_ACCEPT_HEADER" -H "$GH_AUTH_HEADER" ${GH_DOMAIN}/repos/${SRC_GH_USER}/${SRC_GH_REPO}/labels?per_page=100 | jq '[ .[] | { "name": .name, "color": .color, "description": .description } ]' | jq -r '.[] | @base64' )
# for each label from source repo,
# invoke github api to create or update
# the label in the target repo
for sourceLabelJson64 in $sourceLabelsJson64; do
# base64 decode the json
sourceLabelJson=$(echo ${sourceLabelJson64} | base64 --decode | jq -r '.')
# try to create the label
# POST /repos/:owner/:repo/labels { name, color, description }
# https://developer.github.com/v3/issues/labels/#create-a-label
createLabelResponse=$(echo $sourceLabelJson | curl --silent -X POST -d @- -H "$GH_ACCEPT_HEADER" -H "$GH_AUTH_HEADER" ${GH_DOMAIN}/repos/${TGT_GH_USER}/${TGT_GH_REPO}/labels)
# if creation failed then the response doesn't include an id and jq returns 'null'
createdLabelId=$(echo $createLabelResponse | jq -r '.id')
# if label wasn't created maybe it's because it already exists, try to update it
if [ "$createdLabelId" == "null" ]
updateLabelResponse=$(echo $sourceLabelJson | curl --silent -X PATCH -d @- -H "$GH_ACCEPT_HEADER" -H "$GH_AUTH_HEADER" ${GH_DOMAIN}/repos/${TGT_GH_USER}/${TGT_GH_REPO}/labels/$(echo $sourceLabelJson | jq -r '.name | @uri'))
echo "Update label response:\n"$updateLabelResponse"\n"
echo "Create label response:\n"$createLabelResponse"\n"

Browser Automation with JavaScript

In contrast to using the GitHub API to copy labels from one repo to another, another trick I found was simply using JavaScript in your browser’s console to automate clicks on the GitHub website.

Early in my search I came across this gist by Max Thirouin for exporting labels and this gist by Chang-Hsi Hsieh for importing labels. The original scripts didn’t support label descriptions, but folks in the comments of both gists provided updates.

Compared to the “one command and you’re done” approach of the Command Line Automation with Bash solution, the browser automation technique requires you to manually run JavaScript in your browser’s console to export labels and then run some other JavaScript to import labels. This is because the scripts work by assuming you have already navigated in your browser to the labels page of the repository whose labels you want to export from or import into. However, the tradeoff is that you don’t have to provide your GitHub password or access token to third-party code. Everything happens in your browser while already securely logged in to GitHub.

* Inspired by @MoOx original script: https://gist.github.com/MoOx/93c2853fee760f42d97f
* Adds file download per @micalevisk https://gist.github.com/MoOx/93c2853fee760f42d97f#gistcomment-2660220
* Changes include:
* - Get the description from the `title` attribute instead of `aria-label` (doesn't exist anymore)
* - Use style.backgroundColor and parse the rgb(...) to hex (rather than regex parsing of 'style' string)
* - Downloads labels to a JSON file named after the webpage to know which GitHub repo they came from.
* Last tested 2019-July-27:
* - Chrome 75.0.3770.142
* - Safari 12.1.2
* - macOS 10.14.6
function exportGitHubLabels() {
let labels = [];
[].slice.call( document.querySelectorAll( '.label-link' ) )
.forEach( element => {
name: element.textContent.trim(),
description: element.getAttribute( 'title' ),
color: (
// style.backgroundColor returns "rgb(...)"
// but GitHub expects hex when we import the colors
// grab between 'rgb(' and ')'
.substring( 4, element.style.backgroundColor.length - 1 )
// convert comma-delimited string into array of three numbers
.split( ',' )
// reduce array of three numbers into single hex color
.reduce( ( hexValue, rgbValue ) => {
return (
// append next two-digit hex value
hexValue +
// convert decimal to hex
Number( rgbValue ).toString( 16 )
// each number in a hex color is two characters
.padStart( 2, '0' )
}, '' )
return labels;
function saveDataAsJSON( data, filename ) {
const blob = new Blob( [ JSON.stringify( data, null, 4 ) ], { type: 'text/json' } );
const a = document.createElement( 'a' );
a.download = filename;
a.href = window.URL.createObjectURL( blob );
a.dataset.downloadurl = [ 'text/json', a.download, a.href ].join( ':' );
saveDataAsJSON( exportGitHubLabels(), document.title + '.json' );
* Inspired by @Isaddo original script: https://gist.github.com/Isaddo/7efebcb673a0957b9c6f07cd14826ea4
* Adds descriptions per @NillerMedDild https://gist.github.com/Isaddo/7efebcb673a0957b9c6f07cd14826ea4#gistcomment-2715349
* Changes include:
* - CSS selectors use `js` prefix
* Last tested 2019-July-27:
* - Chrome 75.0.3770.142
* - Safari 12.1.2
* - macOS 10.14.6
function createLabel( label ) {
document.querySelector( '.js-new-label-name-input' ).value = label.name;
document.querySelector( '.js-new-label-description-input' ).value = label.description;
document.querySelector( '.js-new-label-color-input' ).value = '#' + label.color;
document.querySelector( '.js-details-target ~ .btn-primary' ).disabled = false;
document.querySelector( '.js-details-target ~ .btn-primary' ).click();
function updateLabel( label ) {
let updatedLabel = false;
[].slice.call( document.querySelectorAll( '.js-labels-list-item' ) ).forEach( element => {
if ( element.querySelector( '.js-label-link' ).textContent.trim() === label.name ) {
updatedLabel = true;
element.querySelector( '.js-edit-label' ).click();
element.querySelector( '.js-new-label-name-input' ).value = label.name;
element.querySelector( '.js-new-label-description-input' ).value = label.description;
element.querySelector( '.js-new-label-color-input' ).value = '#' + label.color;
element.querySelector( '.js-edit-label-cancel ~ .btn-primary' ).click();
return updatedLabel;
function createOrUpdate( label ) {
if ( !updateLabel( label ) ) {
createLabel( label );
].forEach( label => createOrUpdate( label ) );
"name": "a11y",
"description": "This needs improved accessibility. https://a11yproject.com/",
"color": "20e5d8"
"name": "bug: crash đŸ’Ĩ",
"description": "App crashes and is unusable in a significant way.",
"color": "fbca04"
"name": "bug: regression ↩ī¸",
"description": "A new version of the app broke something.",
"color": "fbca04"
"name": "bug 🐞",
"description": "App is not working correctly.",
"color": "fbca04"
"name": "discussion đŸ’Ŧ",
"description": "Discussing usage, best practices, questions, etc.",
"color": "e99695"
"name": "documentation 📓",
"description": "Related to improving documentation and the wiki.",
"color": "6addf7"
"name": "enhancement ✨",
"description": "A new feature suggestion or improvement upon an existing feature.",
"color": "84b6eb"
"name": "salesforce known issue ⚠ī¸",
"description": "A known issue with the Salesforce platform and likely not something the app can address.",
"color": "fcec85"
"name": "security 🔒",
"description": "Questions, concerns, or suggestions for improving the security of the app.",
"color": "1d76db"
"name": "status: in progress ❇ī¸",
"description": "Currently being developed for a future release.",
"color": "c2e0c6"
"name": "status: on hold 💤",
"description": "Work has stopped. There may be one or more blockers or needs more information.",
"color": "c2e0c6"
"name": "status: resolved ✅",
"description": "This has been addressed in a branch and will be scheduled for release in the assigned milestone.",
"color": "c2e0c6"

Honorable Mentions

These are the other options that I considered but ultimately didn’t use.

Probot logo

Probot Settings App

Probot apps are GitHub apps to automate and improve your workflow. One such app is the Settings app. It reads configuration from a .github/settings.yml on your default branch then applies the changes to your GitHub repo.

For example, you can specify whether the repo should be public or private, what the repo’s name and description should be, who should be collaborators, what labels to use, and much more.

The following screenshot is a snippet from .github/settings.yml that defines labels, their colors, and even how to rename a label.

However, a major caveat with the Probot Settings app is that it inherently escalates anyone with push permissions to the admin role, since they can push changes to the settings.yml file to the master branch, which will then apply changes to the repo itself.

In the end, I chose not to use the Probot Settings app due to this security concern and because I wanted an even more light-weight solution than installing a third-party app to my GitHub account.


GitHub Label Maker is an open source web app to create, update, delete, and copy labels from one repository to another. It’s developed by Destan Sarpkaya. At the time of writing, this app uses the GitHub REST API but only supports managing label names and colors. It has not been updated to support managing label descriptions.

The web page is easy enough to use, but I chose not to use it because I want my label descriptions and I didn’t feel comfortable entering my GitHub password into the website.


Copy GitHub Labels is an open source Node.js CLI module to copy labels from one repository to another. It’s developed by Jurgen Van de Moere. At the time of writing, this app uses the official GitHub Node.js library, octokit/rest.js, to interact with the GitHub API. Feature wise, the app only creates new labels, it won’t update existing labels in the destination repo. The project is well documented and has lots of examples to follow.

In the end, I chose not to use this solution because I wanted something even more light-weight than installing a bunch of node modules.


GitHub Copy Labels is an open source Node.js CLI module to copy labels from one repository to another. It’s developed by Harminder Virk. At the time of writing, this app uses a Node.js GitHub library, octonode, for interacting with the GitHub API. Feature wise, the app only creates new labels, it won’t update existing labels in the destination repo.

I actually couldn’t get this app to install or work. Attempting to install the package via npm per the README gave an error about missing file build/bin.js. The demo video looks promising though and ironically uses the build/bin.js file that npm complained about not finding, so your mileage may vary.