Automate GitHub API Calls With Ruby, Keyboard Maestro, and 1Password CLI
Tags: ruby, productivity, github, tutorial
Published: 2022-09-07 15:38:59

One of the perks of the "Ultimate" version of [Ruby on Mac](https://www.rubyonmac.dev/#ultimate) is access to the private GitHub repo. As a developer — especially one who loves automation — it was tempting to try to completely automate inviting new Ultimate customers to the repo.

To do that would require implementing a custom checkout that captures the customer's GitHub username, so I can then pass it on to the [Paddle](https://paddle.com) checkout flow. Paddle unfortunately doesn't support adding custom fields to their checkout.

From there, I would only need to add a few lines of code to my existing small Rails app that receives the Paddle webhook. I'm currently using it to automatically add/update customers in my [ConvertKit](https://convertkit.com) account, so I can easily segment them based on which product they bought, calculate their lifetime value, auto-populate a field with their upgrade coupon, and other useful things.

After extracting the GitHub username from the Paddle payload, I would use the [octokit](https://github.com/octokit/octokit.rb) gem to add the customer as a read-only collaborator to the repo. Something like this:

```ruby
client = Octokit::Client.new(access_token: github_token)
client.add_collaborator(repo, username, permission: 'pull')
```

Given that I'm only getting a few orders of Ultimate per week, I thought I would practice [the fine art of flintstoning](https://stackingthebricks.com/the-fine-art-of-flintstoning/) and invite each user manually for now. However, that doesn't mean I have to do it the slow way each time. 

When a customer emails me to request access, all I have to do is copy their username from the email they sent me, then I press `⌃-⌥-⌘-A`, and it's done! 
This automation uses [Keyboard Maestro](https://www.keyboardmaestro.com/main/), the GitHub API, and [1Password CLI](https://developer.1password.com/docs/cli/).

Here's what the Keyboard Maestro macro looks like:

<a href="https://www.moncefbelyamani.com/images/km_macro_add_github_collaborator.png" target="_blank"><img src="https://www.moncefbelyamani.com/images/km_macro_add_github_collaborator-compressed.jpg" alt="Keyboard Maestro Macro to add a collaborator to a private GitHub repo" width="750" height="660"/></a>

While you can run scripts directly in Keyboard Maestro, I chose to run the script from the project folder in iTerm because the octokit gem is already installed there. Here's what the `add_collab.rb` file looks like:

```ruby
require 'octokit'

def repo
  "rubyonmac/rom-ultimate"
end

def github_token
  `op item get "add_collab GH token" --fields label=notesPlain`
end

def client
  @client ||= Octokit::Client.new(access_token: github_token)
end

customer_github_username = ARGV[0]

puts "adding collaborator #{customer_github_username}"
response = client.add_collaborator(repo, customer_github_username, permission: 'pull')
puts "response: #{response}"
```

And here's the `Gemfile`:

```ruby
source "https://rubygems.org"
ruby File.read(".ruby-version").strip
gem "octokit"
```

If you've read my previous [automation guides featuring Keyboard Maestro](https://www.moncefbelyamani.com/tags/keyboard-maestro), you'll recall that it comes with many handy [tokens](https://wiki.keyboardmaestro.com/Tokens) that are placeholders for data that would otherwise require complicated code to fetch. In this case, I'm using the `%SystemClipboard%` token to pass in the username (that I copied from the customer's email) as an argument to the Ruby script. When you pass an argument to a Ruby script, you can access it via `ARGV[0]`.

For security reasons, I need to provide the Octokit gem with a valid GitHub token associated with my account to be able to make this particular GitHub API call. The way I created this token was by adding a new [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) to my GitHub account, and giving it the appropriate scopes: `admin:org` and `repo`. I also set it to expire after 30 days.

Because I use two Macs at home, I keep all my projects on GitHub so I can easily have the latest code on both computers. However, I gitignore files that contain secrets (like GitHub tokens), even on my private repos. I do this for added security, and also out of habit.

In the past, this would require copying the secret file (such as `.envrc` if using [direnv](https://direnv.net)) from one computer to the other, and then updating it on both computers each time I renew it. It also requires remembering to back up the gitignored file on an external drive if I ever replace my Macs.

But now that I discovered the 1Password CLI, I can get rid of `.envrc`, and I don't need to worry about copying files back and forth or backing anything up. I can fetch the token from my 1Password account, which is automatically available on both computers. 

What I like most about this approach is that **I no longer need to have any secrets stored in plain text on my computer (except in 1Password)!** I can also safely make this repo public if I wanted to.

So, instead of the usual `ENV['GITHUB_TOKEN']`, I can fetch the token with the 1Password CLI `op` tool:

```ruby
def github_token
  `op item get "add_collab GH token" --fields label=notesPlain`
end
```

In Ruby, you can run shell commands by surrounding them with backticks. You might also be familiar with the [system](https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-system) command, but it doesn't return the output of the command. It returns `true` if the command succeeds (with a zero exit status). Since I want the actual output of the command, I need the backticks.

As you might guess from the `op` command, the token is stored in a Secure Note in 1Password called "add_collab GH token". I figured out the full command by reading the documentation for [item get](https://developer.1password.com/docs/cli/reference/management-commands/item#item-get), and then running just this command at first:

```shell
op item get "add_collab GH token"
```

which returned something like this:

```
ID:          some_unique_id
Title:       add_collab GH token
Vault:       Personal
Created:     4 days ago
Updated:     4 days ago by Moncef Belyamani
Favorite:    false
Version:     1
Category:    SECURE_NOTE
Fields:
  notesPlain:    my_github_token
```

That's how I knew that the `label` I needed was `notesPlain`. Here's the full command again:

```shell
op item get "add_collab GH token" --fields label=notesPlain
```

To make this more robust, I could redirect `stderr` to `stdout` by adding `2>&1` to the end of the command, then store the result in a variable, and only call the GitHub API if there's no error. Something like this:

```ruby
def github_token
  token = `op item get "add_collab GH token" --fields label=notesPlain 2>&1`
  if token.include?("ERROR")
    puts "Failed to fetch token from 1Password: #{token}"
    nil
  else
    token
  end
end

def client
  @client ||= Octokit::Client.new(access_token: github_token)
end

if github_token
  puts "adding collaborator #{customer_github_username}"
  response = client.add_collaborator(repo, customer_github_username, permission: 'pull')
  puts "response: #{response}"
end
```

The reason for redirecting `stderr` to the output is to be able to read and store the error message. Without the redirection, if there's an error, `token` will just be an empty string.

Alternatively, I could still use `direnv` by calling the `op` command in `.envrc`, like this:

```shell
export GITHUB_TOKEN=`op item get "add_collab GH token" --fields label=notesPlain`
```

Since there's no secrets in it anymore, I would be able to commit `.envrc` to the repo. And if there's an error fetching the token, I would see it after running `direnv allow`. This approach would be handy for projects that depend on secrets being stored in env vars, like the `RAILS_MASTER_KEY` in Rails apps. 

To save even more time, I could automate the process of verifying that the person who is requesting access did indeed buy [Ruby on Mac](https://www.rubyonmac.dev/#ultimate) Ultimate. Again, Keyboard Maestro makes this easy with the `%MailSender%` token. 

I would then pass this email address as a second argument to my Ruby script, and then use the ConvertKit API via the [convertkit-ruby](https://github.com/hookengine/convertkit-ruby) gem to see if there's an existing entry for that email address, and that the custom field that indicates they purchased Ultimate is filled in.

There you have it. Thanks to Keyboard Maestro, 1Password CLI, the Octokit gem, and a few lines of Ruby, I save about 30 seconds per customer compared to doing everything manually via the GitHub site. With 52 Ultimate customers so far (I just launched the Ultimate version in late July 2022), that's almost 30 minutes saved so far!

If you enjoyed this post and want more free automation ideas and tutorials, [sign up for my weekly newsletter](https://www.moncefbelyamani.com/newsletter).