require 'hoe'
require 'tempfile'
require 'net/http'
require 'net/https' # for Ruby 1.8
require 'uri'
##
# The travis plugin for Hoe manages your .travis.yml file for you in a clean
# and extensible way you can use across projects or by through integration
# with other Hoe plugins.
#
# == Setup
#
# The travis plugin can be used without this setup. By following these
# instructions you can enable and disable a travis-ci hook for your ruby
# projects from rake through rake travis:enable
and rake
# travis:disable
.
#
# === Github API access
#
# Set your github username and password in your ~/.gitconfig:
#
# git config --global github.user username
# git config --global github.password password
# chmod 600 ~/.gitconfig
#
# === Travis token
#
# As of this writing there isn't an easy way to retrieve the travis token
# programmatically. You can find your travis token at
# http://travis-ci.org/profile underneath your github username and email
# address.
#
# To set this in your hoerc run rake config_hoe
and edit the
# "token:" entry.
#
# == Tasks
#
# You can extend the following tasks in your Rakefile or Hoe plugins to add
# extra checks to travis-ci.
#
# travis::
# Run by travis-ci. Defaults to running your tests and checking your
# manifest file. You can run this locally to check what travis-ci will do.
#
# travis:before::
# Runs as the before_script on travis-ci. Defaults to installing your
# development dependencies.
#
# travis:check::
# Lints your .travis.yml.
#
# travis:edit::
# Pulls up your .travis.yml in your EDITOR and lints your configuration upon
# saving. Does not allow you to save a bad .travis.yml.
#
# travis:generate::
# Generates a .travis.yml based on your Hoe spec and .hoerc then brings it
# up in your EDITOR and lints upon saving. Does not allow you to save a bad
# .travis.yml.
#
# travis:enable::
# Enables the travis hook on github.com. Requires further setup as
# described below.
#
# travis:disable::
# Disables the travis hook on github.com. Requires further setup as
# described below.
#
# travis:force::
# Forces a travis-ci run, equivalent to clicking the "test" button on the
# travis-ci hook page.
#
# == Hoe Configuration
#
# The Hoe configuration is used to generate the .travis.yml. After you've
# generated a .travis.yml you may any changes you wish to it and the following
# defaults will not apply. If you have multiple projects, setting up a common
# custom configuration in ~/.hoerc can save you time.
#
# The following default configuration options are provided under the "travis"
# key of the Hoe configuration (accessible from Hoe#with_config):
#
# before_script::
# Array of commands run before the test script. Defaults to installing
# hoe-travis and its dependencies (rake and hoe) followed by running the
# travis:before rake task.
#
# script::
# Runs the travis rake task.
#
# token::
# Your travis-ci token. See @Setup above
#
# versions::
# The versions of ruby used to run your tests. Note that if you have
# multiruby installed, your installed versions will be preferred over the
# defaults that come with hoe-travis.
#
# In your .hoerc you may provide a "notifications" key such as:
#
# travis:
# notifications:
# irc: "irc.example#your_channel"
#
# Notifications specified in a .hoerc will override the default email
# notifications created from the Hoe spec.
module Hoe::Travis
##
# This version of Hoe::Travis
VERSION = '1.3.1'
YAML_EXCEPTIONS = if defined?(Psych) then # :nodoc:
if Psych.const_defined? :Exception then
[Psych::SyntaxError] # Ruby 1.9.2
else
[Psych::Exception, Psych::SyntaxError]
end
else
[YAML::Error]
end
YAML_EXCEPTIONS << ArgumentError
Hoe::DEFAULT_CONFIG['travis'] = {
'before_script' => [
'gem install hoe-travis --no-rdoc --no-ri',
'rake travis:before -t',
],
'after_script' => [
'rake travis:after -t',
],
'script' => 'rake travis',
'token' => 'FIX - See: ri Hoe::Travis',
'versions' => %w[
2.1.0
2.2.0
2.3.0
],
}
def initialize_travis # :nodoc:
@github_api = URI 'https://api.github.com'
end
##
# Adds travis tasks to rake
def define_travis_tasks
desc "Runs your tests for travis"
task :travis => %w[test check_manifest]
namespace :travis do
desc "Run by travis-ci after running the default checks"
task :after => %w[
travis:fake_config
]
desc "Run by travis-ci before running the default checks"
task :before => %w[
install_plugins
travis:install_deps
]
desc "Lint your .travis.yml"
task :check do
abort unless travis_yml_check '.travis.yml'
end
desc "Disables the travis-ci hook"
task :disable do
travis_disable
end
desc "Brings .travis.yml up in your EDITOR then checks it on save"
task :edit do
Tempfile.open 'travis.yml' do |io|
io.write File.read '.travis.yml'
io.rewind
ok = travis_yml_edit io.path
travis_yml_write io.path if ok
end
end
desc "Enables the travis-ci hook"
task :enable do
travis_enable
end
desc "Triggers the travis-ci hook"
task :force do
travis_force
end
task :fake_config do
travis_fake_config
end
desc "Generates a new .travis.yml and allows you to customize it with your EDITOR"
task :generate do
Tempfile.open 'travis.yml' do |io|
io.write travis_yml_generate
io.rewind
ok = travis_yml_edit io.path
travis_yml_write io.path if ok
end
end
task :install_deps do
(extra_deps + extra_dev_deps).each do |dep|
begin
gem(*dep)
rescue Gem::LoadError
name, req, = dep
install_gem name, req, false
end
end
end
end
end
##
# Extracts the travis after_script from your .hoerc
def travis_after_script
with_config { |config, _|
config['travis']['after_script'] or
Hoe::DEFAULT_CONFIG['travis']['after_script']
}
end
##
# Extracts the travis before_script from your .hoerc
def travis_before_script
with_config { |config, _|
config['travis']['before_script'] or
Hoe::DEFAULT_CONFIG['travis']['before_script']
}
end
##
# Disables travis-ci for this repository.
def travis_disable
_, repo, = travis_github_check
if hook = travis_have_hook?(repo) then
travis_edit_hook repo, hook, false if hook['active']
end
end
##
# Edits the travis +hook+ definition for +repo+ (from the github URL) to
# +enable+ (default) or disable it.
def travis_edit_hook repo, hook, enable = true
patch = unless Net::HTTP.const_defined? :Patch then
# Ruby 1.8
Class.new Net::HTTPRequest do |c|
c.const_set :METHOD, 'PATCH'
c.const_set :REQUEST_HAS_BODY, true
c.const_set :RESPONSE_HAS_BODY, true
end
else
Net::HTTP::Patch
end
id = hook['id']
body = {
'name' => hook['name'],
'active' => enable,
'config' => hook['config']
}
travis_github_request "/repos/#{repo}/hooks/#{id}", body, patch
end
##
# Enables travis-ci for this repository.
def travis_enable
user, repo, token = travis_github_check
if hook = travis_have_hook?(repo) then
travis_edit_hook repo, hook unless hook['active']
else
travis_make_hook repo, user, token
end
end
##
# Creates a fake config file for use on travis-ci. Running this with a
# pre-existing .hoerc has no effect.
def travis_fake_config
fake_hoerc = File.expand_path '~/.hoerc'
return if File.exist? fake_hoerc
config = { 'exclude' => /\.(git|travis)/ }
open fake_hoerc, 'w' do |io|
YAML.dump config, io
end
end
##
# Forces the travis-ci hook
def travis_force
user, repo, token = travis_github_check
unless hook = travis_have_hook?(repo)
hook = travis_make_hook repo, user, token
end
travis_github_request "/repos/#{repo}/hooks/#{hook['id']}/test", {}
end
##
# Ensures you have proper setup for editing the github travis hook
def travis_github_check
user = `git config github.user`.chomp
abort <<-ABORT unless user
Set your github user and token in ~/.gitconfig
See: ri Hoe::Travis and
\thttp://help.github.com/set-your-user-name-email-and-github-token/
ABORT
`git config remote.origin.url` =~ /^git@github\.com:(.*).git$/
repo = $1
abort <<-ABORT unless repo
Unable to determine your github repository.
Expected \"git@github.com:[repo].git\" as your remote origin
ABORT
token = with_config do |config, _|
config['travis']['token']
end
abort 'Please set your travis token via `rake config_hoe` - ' \
'See: ri Hoe::Travis' if token =~ /FIX/
return user, repo, token
end
##
# Makes a github request at +path+ with an optional +body+ Hash which will
# be sent as JSON. The default +method+ without a body is a GET request,
# otherwise POST.
def travis_github_request(path, body = nil,
method = body ? Net::HTTP::Post : Net::HTTP::Get)
begin
require 'json'
rescue LoadError => e
raise unless e.message.end_with? 'json'
abort 'Please gem install json like modern ruby versions have'
end
uri = @github_api + path
http = Net::HTTP.new uri.host, uri.port
http.use_ssl = uri.scheme.downcase == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.cert_store = OpenSSL::X509::Store.new
http.cert_store.set_default_paths
req = method.new uri.request_uri
if body then
req.content_type = 'application/json'
req.body = JSON.dump body
end
user = `git config github.user`.chomp
pass = `git config github.password`.chomp
req.basic_auth user, pass
res = http.request req
body = JSON.parse res.body if res.class.body_permitted?
unless Net::HTTPSuccess === res then
message = ": #{res['message']}" if body
raise "github API error #{res.code}#{message}"
end
body
end
##
# Returns the github hook definition for the "travis" hook on +repo+ (from
# the github URL), if it exists.
def travis_have_hook? repo
body = travis_github_request "/repos/#{repo}/hooks"
body.find { |hook| hook['name'] == 'travis' }
end
##
# Creates a travis hook for +user+ on the given +repo+ (from the github URL)
# that uses the users +token+.
def travis_make_hook repo, user, token
body = {
"name" => "travis",
"active" => true,
"config" => {
"domain" => "",
"token" => token,
"user" => user,
}
}
travis_github_request "/repos/#{repo}/hooks", body
end
##
# Creates the travis notifications hash from the developers for your
# project. The developer will be merged with the travis notifications from
# your .hoerc.
def travis_notifications
email = @email.compact
email.delete ''
default_notifications = { 'email' => email }
notifications = with_config do |config, _|
config['travis']['notifications'] or
Hoe::DEFAULT_CONFIG['travis']['notifications']
end || {}
default_notifications.merge notifications
end
##
# Extracts the travis script from your .hoerc
def travis_script
with_config { |config, _|
config['travis']['script'] or
Hoe::DEFAULT_CONFIG['travis']['script']
}
end
##
# Determines the travis versions from multiruby, if available, or your
# .hoerc.
def travis_versions
if have_gem? 'ZenTest' and
File.exist?(File.expand_path('~/.multiruby')) then
`multiruby -v` =~ /^Passed: (.*)/
$1.split(', ').map do |ruby_release|
ruby_release.sub(/-.*/, '')
end
else
with_config do |config, _|
config['travis']['versions'] or
Hoe::DEFAULT_CONFIG['travis']['versions']
end
end.sort
end
##
# Submits the travis.yml in +path+ to travis-ci.org for linting. If the
# file is OK true is returned, otherwise the issues are displayed on $stderr
# and false is returned.
def travis_yml_check path
require 'net/http'
post_body = {
'content' => File.read(path),
}
req = Net::HTTP::Post.new '/lint'
req.set_form post_body, 'multipart/form-data'
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
http = Net::HTTP.new 'api.travis-ci.org', 443
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.cert_store = cert_store
res = http.request req
unless Net::HTTPOK === res then
warn "Unable to lint #{path}: #{res.body}"
return false
end
require 'json'
response = JSON.parse res.body
lint = response.fetch 'lint'
warnings = lint.fetch 'warnings'
return true if warnings.empty?
warnings.each do |warning|
keys = warning.fetch 'key'
message = warning.fetch 'message'
if keys.empty? then
warn message
else
warn "For #{keys.join ', '}: #{message}"
end
end
return false
rescue Net::HTTPError => e
warn "Unable to lint #{path}: #{e.message}"
return false
end
##
# Loads the travis.yml in +path+ in your EDITOR (or vi if unset). Upon
# saving the travis.yml is checked by linting. If any problems are found
# you will be asked to retry the edit.
#
# If the edited travis.yml is OK true is returned, otherwise false.
def travis_yml_edit path
loop do
editor = ENV['EDITOR'] || 'vi'
system "#{editor} #{path}"
break true if travis_yml_check path
abort unless $stdout.tty?
print "\nRetry edit? [Yn]\n> "
$stdout.flush
break false if $stdin.gets =~ /\An/i
end
end
##
# Generates a travis.yml from .hoerc, the Hoe spec and the default
# configuration.
def travis_yml_generate
travis_yml = {
'after_script' => travis_after_script,
'before_script' => travis_before_script,
'language' => 'ruby',
'notifications' => travis_notifications,
'rvm' => travis_versions,
'script' => travis_script,
}
travis_yml.each do |key, value|
travis_yml.delete key unless value
end
YAML.dump travis_yml
end
##
# Writes the travis.yml in +source_file+ to .travis.yml in the current
# directory. Overwrites an existing .travis.yml.
def travis_yml_write source_file
open source_file do |source_io|
open '.travis.yml', 'w' do |dest_io|
dest_io.write source_io.read
end
end
end
end