Running SSH Commands via Rakefiles

TL;DR: CocoaDocs' commands which used to be executed via logging in are now done via rake commands.

CocoaDocs runs a lot of CocoaPods' infrastructure. It is the workhorse that allows us to provide rendered READMEs/CHANGELOGs, QIs and status updates.

I have been manually sshing in to the server to execute commands for a few years, and it's a bit of a chore. Over time though, I became used to this. As more people were starting to help out in maintaining CocoaDocs, it became obvious that it needed to change from being dark arcane knowledge to easy -discoverable- commands.

Show me the code: c7be695...a00338d.

I initially explored the idea of using Mina to let us treat CocoaDocs like we do with heroku. I spent a few hours setting it up, but never managed to get a single working prototype. Eventually I started looking for alternatives.

I found net-ssh, which is a pleasant reminder of how old the ruby community is, given that docs are from 2004. I used it to connect to the server in a tiny amount of code:

require 'net/ssh'

Net::SSH.start('api.cocoadocs.org', 'cocoadocs', :password => "hahayeahright") do |ssh|
  output = ssh.exec!("hostname")
  puts output
end

Which was enough to start. Once I started though, I hit a problem. Each command is executed stateless-ly. What this means, in principal, is that your session won't stay in the same folder, and you won't have your normal shell setup. I started out trying to do all the things a shell would do source ~/.zshrc etc. etc. Not practical.

After giving up on the exec function, I did some duckduckgo-ing for alternatives and found a chapter on shell in the docs. However, the docs were out of date, and the code didn't exist. Luckily it had just been separated out into a new gem: net-ssh-shell. This meant we could do:

require 'net/ssh'
require 'net/ssh/shell'

Net::SSH.start('api.cocoadocs.org', 'cocoadocs', :password => "hahayeahright") do |ssh|
  ssh.shell do |sh|
    sh.execute 'hostname'
  end
end

Which executes the command, as expected, as though you had logged in. Perfect. With that, I did some minor refactoring to turn it all into a function that takes some commands as an array.

 def run_ssh_commands commands
   puts "Connecting to api.cocoadocs.org:"
   Net::SSH.start('api.cocoadocs.org', 'cocoadocs') do |ssh|
     ssh.shell do |sh|
       sh.execute 'cd cocoadocs.org'
       commands.each do |command|
         puts command.yellow
         sh.execute command
       end
       sh.execute 'exit'
     end
   end
  end

I included an exit command to close the session at the end of the submitted commands, and that's it. Commands are now easily run from a maintainer's computer without the need for domain knowledge. They just need to know that bundle exec rake -T will give useful tasks. Given that we use Rakefile in all projects, this knowledge is implicit. Woo!

So, next time you hear people complaining about a chore you've become used to, take heed and try to automate the problem away.