Automate GitHub API Calls With Ruby, Keyboard Maestro, and 1Password CLI

2 years ago 32

One of the perks of the “Ultimate” version of Ruby on Mac 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 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 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 gem to add the customer as a read-only collaborator to the repo. Something like this: 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 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, the GitHub API, and 1Password CLI. Here’s what the Keyboard Maestro macro looks like: 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: 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: source "https://rubygems.org" ruby File.read(".ruby-version").strip gem "octokit" If you’ve read my previous automation guides featuring Keyboard Maestro, you’ll recall that it comes with many handy 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 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) 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: 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 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, and then running just this command at first: 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: 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: 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: 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 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 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 49 Ultimate customers so far (I just launched the Ultimate version about a month ago in late July 2022), that’s about 25 minutes saved so far!


View Entire Post

Read Entire Article