Skip to content
This repository has been archived by the owner on Aug 1, 2022. It is now read-only.

Introduce automatic deployments for GitHub projects #39

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ group :test do
gem 'vcr', '~> 2'
gem 'webmock'
gem "fakefs", :require => "fakefs/safe"
gem 'test_after_commit'
end
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ GEM
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.5)
temple (0.4.0)
test_after_commit (0.0.1)
therubyracer (0.10.2)
libv8 (~> 3.3.10)
thor (0.14.6)
Expand Down Expand Up @@ -290,6 +291,7 @@ DEPENDENCIES
sinatra
slim
sqlite3
test_after_commit
twitter-bootstrap-rails
uglifier (>= 1.0.3)
vcr (~> 2)
Expand Down
38 changes: 38 additions & 0 deletions app/controllers/listener_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class ListenerController < ApplicationController
before_filter :validate_signature

def github
if params[:ref] == 'refs/heads/master'
repo_name = params[:repository][:name]
owner_name = params[:repository][:owner][:name]
project_url = "[email protected]:#{owner_name}/#{repo_name}.git"
if project = Project.find_by_url(project_url)
project.jobs.create({:task => 'deploy', :branch => 'master'})
end
end
head :no_content
end

private

def validate_signature
if signature = request.headers['HTTP_X_HUB_SIGNATURE']
digest = OpenSSL::Digest::Digest.new("sha1")
signature = signature.split("=").last

sig = OpenSSL::HMAC.hexdigest(
digest,
Strano.github_hook_secret,
request.body.read
)

return true if signature == OpenSSL::HMAC.hexdigest(
digest,
Strano.github_hook_secret,
request.body.read
)
end

head :unauthorized
end
end
2 changes: 1 addition & 1 deletion app/models/job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Job < ActiveRecord::Base

belongs_to :project
belongs_to :user
after_create :execute_task
after_commit :execute_task, :on => :create

default_scope order('created_at DESC')
default_scope where(:deleted_at => nil)
Expand Down
9 changes: 8 additions & 1 deletion app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class Project < ActiveRecord::Base
validate :url, :presence => true, :uniqueness => { :case_sensitive => false }

before_create :ensure_allowed_repo
after_create :clone_repo
after_commit :clone_repo, :on => :create
after_commit :create_hook, :on => :create
before_save :update_data
after_destroy :remove_repo

Expand Down Expand Up @@ -151,6 +152,12 @@ def clone_repo
CloneRepo.perform_async id
end

def create_hook
if github? && Strano.base_url
github.hook("#{Strano.base_url}listener", Strano.github_hook_secret)
end
end

def remove_repo
RemoveRepo.perform_async id
end
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
end
end

post '/listener' => 'listener#github'

require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'

Expand Down
8 changes: 8 additions & 0 deletions config/strano.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ defaults: &defaults
# Strano's Github application secret. See https://github.com/settings/applications
github_secret: github-application-secret

# Secret used to sign hook requests from GitHub.
github_hook_secret: github-hook-secret

# The application URL with a trailing slash, required for github hooks.
# Can be passed as environment variable STRANO_BASE_URL=''
#
# base_url: http://example.com/

# The path to where Strano will clone your project's repos.
#
# clone_path: vendor/repos
Expand Down
14 changes: 13 additions & 1 deletion lib/github/repo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ def inspect
end
alias :to_hash :inspect

def hook(url, secret)
post "/repos/#{user_name}/#{repo_name}/hooks", {
"name" => "web",
"active" => true,
"config" => {
"url" => url,
"secret" => secret,
"content_type" => "json"
}
}
end


private

Expand All @@ -20,4 +32,4 @@ def repo
end

end
end
end
12 changes: 11 additions & 1 deletion lib/strano/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Configuration
:clone_path,
:github_key,
:github_secret,
:github_hook_secret,
:base_url,
:allow_organizations,
:allow_users].freeze

Expand All @@ -26,6 +28,12 @@ module Configuration
# https://github.com/account/applications
DEFAULT_GITHUB_SECRET = nil

# Secret used to sign hook requests from GitHub.
DEFAULT_GITHUB_HOOK_SECRET = 'topsecret'

# The application URL with a trailing slash, required for github hooks.
DEFAULT_BASE_URL = nil

# Allow project creation from repos for Github organization accounts.
# Default value is true, which allows any and all organizations. Set to
# false to disallow creating projects from organizations completely.
Expand Down Expand Up @@ -117,9 +125,11 @@ def reset
self.clone_path = DEFAULT_CLONE_PATH
self.github_key = DEFAULT_GITHUB_KEY
self.github_secret = DEFAULT_GITHUB_SECRET
self.github_hook_secret = DEFAULT_GITHUB_HOOK_SECRET
self.base_url = DEFAULT_BASE_URL
self.allow_organizations = DEFAULT_ALLOW_ORGANIZATIONS
self.allow_users = DEFAULT_ALLOW_USERS
self
end
end
end
end
59 changes: 59 additions & 0 deletions spec/controllers/listener_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
require "spec_helper"

# helper method to simulate POST from GitHub
def post_github(request, sig=nil)
# load webhook example from file
payload = File.open(Rails.root.join('spec/fixtures/github/webhook.json'), 'r') { |f| f.read }

# make valid signature if necessary
unless sig
digest = OpenSSL::Digest::Digest.new("sha1")
sig = OpenSSL::HMAC.hexdigest(
digest,
Strano.github_hook_secret,
payload
)
request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{sig}"
end

request.env['HTTP_CONTENT_TYPE'] = 'application/json'
request.env['RAW_POST_DATA'] = payload
post :github, JSON.parse(payload)
end

describe ListenerController do
describe "POST #github" do
context "with a valid signature" do
let(:url) { '[email protected]:yevgenko/cap-foobar.git' }
let(:jobs) { stub(:jobs, create: true) }
let(:project) { Project.new }

before(:each) do
project.stub(:jobs).and_return(jobs)
Project.stub(:find_by_url).and_return(project)
end

it "should respond with 'No Content'" do
post_github(@request)
should respond_with 204
end

it "finds the project" do
Project.should_receive(:find_by_url).with(url)
post_github(@request)
end

it "creates the job" do
jobs.should_receive(:create)
post_github(@request)
end
end

context "with invalid signature" do
it "should respond with 'Unauthorized'" do
post_github(@request, 'invalid-signature')
should respond_with 401
end
end
end
end
45 changes: 45 additions & 0 deletions spec/fixtures/github/webhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"pusher":{"name":"none"},
"repository":{
"name":"cap-foobar",
"size":112,
"created_at":"2012-12-18T02:41:31-08:00",
"has_wiki":false,
"watchers":0,
"private":false,
"fork":false,
"language":"Ruby",
"url":"https://github.com/yevgenko/cap-foobar",
"id":7221723,
"pushed_at":"2012-12-18T02:42:05-08:00",
"open_issues":0,
"has_downloads":true,
"has_issues":false,
"forks":0,
"stargazers":0,
"owner":{
"name":"yevgenko",
"email":"[email protected]"
}
},
"forced":false,
"head_commit":{
"modified":[],
"added":["Capfile","config/deploy.rb"],
"author":{"name":"Yevgeniy A. Viktorov","username":"yevgenko","email":"[email protected]"},
"removed":[],
"timestamp":"2012-12-18T02:41:22-08:00",
"url":"https://github.com/yevgenko/cap-foobar/commit/42ccc8a12d4db45547626e34c048990a21551981",
"id":"42ccc8a12d4db45547626e34c048990a21551981",
"distinct":true,
"message":"Initial commit",
"committer":{"name":"Yevgeniy A. Viktorov","username":"yevgenko","email":"[email protected]"}
},
"after":"42ccc8a12d4db45547626e34c048990a21551981",
"deleted":false,
"commits":[],
"ref":"refs/heads/master",
"compare":"https://github.com/yevgenko/cap-foobar/compare/42ccc8a12d4d...42ccc8a12d4d",
"before":"42ccc8a12d4db45547626e34c048990a21551981",
"created":false
}
15 changes: 15 additions & 0 deletions spec/lib/github/repo_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require "spec_helper"

describe Github::Repo do

let(:url) { '[email protected]:yevgenko/strano.git' }
let(:repo) { Strano::Repo.new(url) }
let(:github) { Github.new('somerandomkey') }

describe "#hook" do
use_vcr_cassette :erb => true

it { github.repo(repo.user_name, repo.repo_name).hook('http://example.com/listener', 'secret') }
end

end
45 changes: 35 additions & 10 deletions spec/models/project_spec.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
require 'spec_helper'

describe Project do

let(:url) { '[email protected]:joelmoss/strano.git' }
let(:user) { FactoryGirl.create(:user) }
let(:cloned_project) { FactoryGirl.build_stubbed(:project) }

before(:each) do
Github.strano_user_token = user.github_access_token
@project = Project.create :url => url
end

it "should set the data after save", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
@project.data.should_not be_empty
end
context "#create" do
let(:project) { Project.create :url => url }

describe "#repo", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
it { @project.repo.should be_a(Strano::Repo) }
end
it "should set the data after save", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
project.data.should_not be_empty
end

describe "#github", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
it { @project.github.should be_a(Github::Repo) }
describe "#repo", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
it { project.repo.should be_a(Strano::Repo) }
end

describe "#github", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
it { project.github.should be_a(Github::Repo) }
end
end

describe "#creat_hook" do
let(:project) { Project.new :url => url }

context "when Strano.base_url is nil", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
it "shouldn't create a hook" do
project.github.should_not_receive(:hook)
end
end

context "when Strano.base_url is not nil", :vcr => { :cassette_name => 'Github_Repo/_repo' } do
before(:each) do
Strano.stub(:base_url).and_return('http://example.com/listener')
end

it "should create a hook" do
project.github.should_receive(:hook)
end
end

after(:each) do
project.save
end
end
end
Loading