├── test
├── custom.xml.erb
├── default.xml.erb
├── commit_test.rb
├── github_status_test.rb
├── repository_test.rb
├── test_helper.rb
└── janky_test.rb
├── Gemfile
├── script
├── bootstrap
├── test
├── server
└── cibuild
├── config.ru
├── lib
├── janky
│ ├── version.rb
│ ├── database
│ │ ├── seed.dump.gz
│ │ ├── migrate
│ │ │ ├── 1317384652_change_commit_message_to_text.rb
│ │ │ ├── 1400144784_change_room_id_to_string.rb
│ │ │ ├── 1317384653_add_build_pusher.rb
│ │ │ ├── 1317384619_add_build_room_id.rb
│ │ │ ├── 1398262033_add_context.rb
│ │ │ ├── 1313871652_add_commit_url_column.rb
│ │ │ ├── 1317384618_add_repo_hook_url.rb
│ │ │ ├── 1317384655_add_template.rb
│ │ │ ├── 1317384649_github_team_id.rb
│ │ │ ├── 1313867551_add_build_output_column.rb
│ │ │ ├── 1312117285_non_unique_repo_uri.rb
│ │ │ ├── 1317384650_add_build_indexes.rb
│ │ │ ├── 1317384629_drop_default_room_id.rb
│ │ │ ├── 1312198807_repo_enabled.rb
│ │ │ ├── 1317384654_add_build_queued_at.rb
│ │ │ ├── 1317384651_add_more_build_indexes.rb
│ │ │ └── 1312115512_init.rb
│ │ └── schema.rb
│ ├── public
│ │ ├── images
│ │ │ ├── logo.png
│ │ │ ├── building-bot.gif
│ │ │ ├── robawt-status.gif
│ │ │ └── disclosure-arrow.png
│ │ ├── javascripts
│ │ │ ├── application.js
│ │ │ └── jquery.relatize.js
│ │ └── css
│ │ │ └── base.css
│ ├── templates
│ │ ├── console.mustache
│ │ ├── index.mustache
│ │ └── layout.mustache
│ ├── views
│ │ ├── layout.rb
│ │ ├── console.rb
│ │ └── index.rb
│ ├── helpers.rb
│ ├── notifier
│ │ ├── failure_service.rb
│ │ ├── chat_service.rb
│ │ ├── multi.rb
│ │ ├── mock.rb
│ │ └── github_status.rb
│ ├── chat_service
│ │ ├── mock.rb
│ │ ├── campfire.rb
│ │ ├── hipchat.rb
│ │ ├── hubot.rb
│ │ └── slack.rb
│ ├── builder
│ │ ├── receiver.rb
│ │ ├── mock.rb
│ │ ├── runner.rb
│ │ ├── http.rb
│ │ ├── payload.rb
│ │ └── client.rb
│ ├── github
│ │ ├── commit.rb
│ │ ├── payload.rb
│ │ ├── payload_parser.rb
│ │ ├── mock.rb
│ │ ├── receiver.rb
│ │ └── api.rb
│ ├── commit.rb
│ ├── tasks.rb
│ ├── exception.rb
│ ├── build_request.rb
│ ├── app.rb
│ ├── notifier.rb
│ ├── chat_service.rb
│ ├── builder.rb
│ ├── job_creator.rb
│ ├── branch.rb
│ ├── github.rb
│ ├── hubot.rb
│ ├── repository.rb
│ └── build.rb
└── janky.rb
├── .gitignore
├── .travis.yml
├── Rakefile
├── COPYING
├── .github
└── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── janky.gemspec
├── CHANGES
└── README.md
/test/custom.xml.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/default.xml.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 | gemspec
3 |
--------------------------------------------------------------------------------
/script/bootstrap:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | bundle install --binstubs
3 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | require "janky"
2 | Janky.setup(ENV)
3 | run Janky.app
4 |
--------------------------------------------------------------------------------
/lib/janky/version.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | VERSION = "0.13.0.pre1"
3 | end
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle
2 | bin
3 | vendor/gems
4 | Gemfile.lock
5 | *.gem
6 | .ruby-version
7 | vendor/bundle
8 | tags
9 |
--------------------------------------------------------------------------------
/lib/janky/database/seed.dump.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/polydice/janky/master/lib/janky/database/seed.dump.gz
--------------------------------------------------------------------------------
/lib/janky/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/polydice/janky/master/lib/janky/public/images/logo.png
--------------------------------------------------------------------------------
/lib/janky/public/images/building-bot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/polydice/janky/master/lib/janky/public/images/building-bot.gif
--------------------------------------------------------------------------------
/lib/janky/public/images/robawt-status.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/polydice/janky/master/lib/janky/public/images/robawt-status.gif
--------------------------------------------------------------------------------
/script/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'test/unit'
3 | exit Test::Unit::AutoRunner.run(true, nil, Dir.glob("test/*_test.rb"))
4 |
--------------------------------------------------------------------------------
/lib/janky/public/images/disclosure-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/polydice/janky/master/lib/janky/public/images/disclosure-arrow.png
--------------------------------------------------------------------------------
/script/server:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | : ${RACK_ENV:="development"}
3 | : ${JANKY_BASE_URL:="http://localhost:9393/"}
4 | export RACK_ENV JANKY_BASE_URL
5 | bin/shotgun -p9393 -sthin -Ilib config.ru
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.3.5
4 | - 2.2.3
5 | - 2.1.7
6 | - 2.0.0
7 | - 1.9.3
8 | matrix:
9 | fast_finish: true
10 | allow_failures:
11 | - rvm: 2.3.5
12 | script: "./script/cibuild"
13 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384652_change_commit_message_to_text.rb:
--------------------------------------------------------------------------------
1 | class ChangeCommitMessageToText < ActiveRecord::Migration
2 | def change
3 | change_column :commits, :message, :text, :null => false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1400144784_change_room_id_to_string.rb:
--------------------------------------------------------------------------------
1 | class ChangeRoomIdToString < ActiveRecord::Migration
2 | def change
3 | change_column :repositories, :room_id, :string
4 | change_column :builds, :room_id, :string
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384653_add_build_pusher.rb:
--------------------------------------------------------------------------------
1 | class AddBuildPusher < ActiveRecord::Migration
2 | def self.up
3 | add_column :builds, :user, :string, :null => true
4 | end
5 |
6 | def self.down
7 | remove_column :builds, :user
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384619_add_build_room_id.rb:
--------------------------------------------------------------------------------
1 | class AddBuildRoomId < ActiveRecord::Migration
2 | def self.up
3 | add_column :builds, :room_id, :integer, :null => true
4 | end
5 |
6 | def self.down
7 | remove_column :builds, :room_id
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1398262033_add_context.rb:
--------------------------------------------------------------------------------
1 | class AddContext < ActiveRecord::Migration
2 | def self.up
3 | add_column :repositories, :context, :string, :null => true
4 | end
5 |
6 | def self.down
7 | remove_column :repositories, :context
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1313871652_add_commit_url_column.rb:
--------------------------------------------------------------------------------
1 | class AddCommitUrlColumn < ActiveRecord::Migration
2 | def self.up
3 | add_column :commits, :url, :string, :null => false
4 | end
5 |
6 | def self.down
7 | remove_column :commits, :url
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/templates/console.mustache:
--------------------------------------------------------------------------------
1 |
2 | {{ repo_name }}/{{ branch_name }}/{{ commit_short_sha }}
3 | {{ jenkins_url }}
4 |
5 | {{ output }}
6 |
--------------------------------------------------------------------------------
/lib/janky/public/javascripts/application.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | function refresh() {
3 | $('.builds').load(location.pathname + ' .builds', function() {
4 | $('.relatize').relatize()
5 | })
6 | }
7 |
8 | setInterval(refresh, 5000)
9 | $('.relatize').relatize()
10 | })
11 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384618_add_repo_hook_url.rb:
--------------------------------------------------------------------------------
1 | class AddRepoHookUrl < ActiveRecord::Migration
2 | def self.up
3 | add_column :repositories, :hook_url, :string, :null => true
4 | end
5 |
6 | def self.down
7 | remove_column :repositories, :hook_url
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384655_add_template.rb:
--------------------------------------------------------------------------------
1 | class AddTemplate < ActiveRecord::Migration
2 | def self.up
3 | add_column :repositories, :job_template, :string, :null => true
4 | end
5 |
6 | def self.down
7 | remove_column :repositories, :job_template
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384649_github_team_id.rb:
--------------------------------------------------------------------------------
1 | class GithubTeamId < ActiveRecord::Migration
2 | def self.up
3 | add_column :repositories, :github_team_id, :integer, :null => true
4 | end
5 |
6 | def self.down
7 | remove_column :repositories, :github_team_id
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1313867551_add_build_output_column.rb:
--------------------------------------------------------------------------------
1 | class AddBuildOutputColumn < ActiveRecord::Migration
2 | def self.up
3 | add_column :builds, :output, :text, :null => true, :default => nil
4 | end
5 |
6 | def self.down
7 | remove_column :builds, :output
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1312117285_non_unique_repo_uri.rb:
--------------------------------------------------------------------------------
1 | class NonUniqueRepoUri < ActiveRecord::Migration
2 | def self.up
3 | remove_index :repositories, :uri
4 | add_index :repositories, :uri
5 | end
6 |
7 | def self.down
8 | add_index :repositories, :uri, :unique => true
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384650_add_build_indexes.rb:
--------------------------------------------------------------------------------
1 | class AddBuildIndexes < ActiveRecord::Migration
2 | def self.up
3 | add_index :builds, :commit_id
4 | add_index :builds, :branch_id
5 | end
6 |
7 | def self.down
8 | remove_index :builds, :commit_id
9 | remove_index :builds, :branch_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384629_drop_default_room_id.rb:
--------------------------------------------------------------------------------
1 | class DropDefaultRoomId < ActiveRecord::Migration
2 | def self.up
3 | change_column :repositories, :room_id, :integer, :default => nil, :null => true
4 | end
5 |
6 | def self.down
7 | change_column :repositories, :room_id, :integer, :default => 376289, :null => false
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/janky/views/layout.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Janky
3 | module Views
4 | class Layout < Mustache
5 | def title
6 | ENV["JANKY_PAGE_TITLE"] || "Janky Hubot"
7 | end
8 |
9 | def page_class
10 | nil
11 | end
12 |
13 | def root
14 | @request.env['SCRIPT_NAME']
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1312198807_repo_enabled.rb:
--------------------------------------------------------------------------------
1 | class RepoEnabled < ActiveRecord::Migration
2 | def self.up
3 | add_column :repositories, :enabled, :boolean, :null => false, :default => true
4 | add_index :repositories, :enabled
5 | end
6 |
7 | def self.down
8 | remove_column :repositories, :enabled
9 | remove_index :repositories, :enabled
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384654_add_build_queued_at.rb:
--------------------------------------------------------------------------------
1 | class AddBuildQueuedAt < ActiveRecord::Migration
2 | def self.up
3 | add_column :builds, :queued_at, :datetime, :null => true
4 | Janky::Build.started.each do |b|
5 | b.update_attributes!(:queued_at => b.created_at)
6 | end
7 | end
8 |
9 | def self.down
10 | remove_column :builds, :queued_at
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/janky/helpers.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Helpers
3 | def self.registered(app)
4 | app.enable :raise_errors
5 | app.disable :show_exceptions
6 | app.helpers self
7 | end
8 |
9 | def find_repo(name)
10 | unless repo = Repository.find_by_name(name)
11 | halt(404, "Unknown repository: #{name.inspect}")
12 | end
13 |
14 | repo
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1317384651_add_more_build_indexes.rb:
--------------------------------------------------------------------------------
1 | class AddMoreBuildIndexes < ActiveRecord::Migration
2 | def self.up
3 | add_index :builds, :started_at
4 | add_index :builds, :completed_at
5 | add_index :builds, :green
6 | end
7 |
8 | def self.down
9 | remove_index :builds, :started_at
10 | remove_index :builds, :completed_at
11 | remove_index :builds, :green
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/commit_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../test_helper", __FILE__)
2 |
3 | class CommitTest < Test::Unit::TestCase
4 | def setup
5 | Janky.setup(environment)
6 | Janky.enable_mock!
7 | Janky.reset!
8 |
9 | DatabaseCleaner.clean_with(:truncation)
10 | end
11 |
12 | test "responds to #last_build" do
13 | assert_respond_to Janky::Commit.new, :last_build
14 | end
15 |
16 | test "responds to #build!" do
17 | assert_respond_to Janky::Commit.new, :build!
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/janky/templates/index.mustache:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/lib/janky/notifier/failure_service.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Notifier
3 | class FailureService < ChatService
4 | def self.completed(build)
5 | return unless need_failure_notification?(build)
6 | ::Janky::ChatService.speak(message(build), failure_room, {:color => color(build)})
7 | end
8 |
9 | def self.failure_room
10 | ENV["JANKY_CHAT_FAILURE_ROOM"]
11 | end
12 |
13 | def self.need_failure_notification?(build)
14 | build.red? && failure_room != build.room_id
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/script/cibuild:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -ex
3 |
4 | RACK_ENV="test"
5 | DATABASE_URL="mysql2://root@localhost/janky_test"
6 | export RACK_ENV DATABASE_URL
7 | export RBENV_VERSION="2.0.0-github"
8 |
9 | test -n "$RBENV_VERSION" && {
10 | export PATH="/usr/share/rbenv/shims:$PATH"
11 | DBNAME=`echo "${RBENV_VERSION}" | sed -e 's/[\.|\-]/_/g'`
12 | export DATABASE_URL="${DATABASE_URL}_${DBNAME}"
13 | }
14 |
15 | hostname
16 | ruby -v
17 | env
18 | bundle install --binstubs --path vendor/gems
19 | mysql -u root -e "CREATE DATABASE IF NOT EXISTS janky_test_${DBNAME}"
20 | bin/rake db:migrate --trace
21 | bundle exec script/test
22 |
--------------------------------------------------------------------------------
/lib/janky/chat_service/mock.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module ChatService
3 | # Mock chat implementation used in testing environments.
4 | class Mock
5 | def initialize
6 | @rooms = {}
7 | end
8 |
9 | attr_writer :rooms
10 |
11 | def speak(room_name, message)
12 | if !@rooms.values.include?(room_name)
13 | raise Error, "Unknown room #{room_name.inspect}"
14 | end
15 | end
16 |
17 | def rooms
18 | acc = []
19 | @rooms.each do |id, name|
20 | acc << Room.new(id, name)
21 | end
22 | acc
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/janky/builder/receiver.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Builder
3 | class Receiver
4 | def self.call(env)
5 | request = Rack::Request.new(env)
6 | default_base_url = Builder[:default].url
7 | payload = Payload.parse(request.body, default_base_url)
8 |
9 | if payload.started?
10 | Build.start(payload.id, payload.url)
11 | elsif payload.completed?
12 | Build.complete(payload.id, payload.green?)
13 | else
14 | return Rack::Response.new("Bad Request", 400).finish
15 | end
16 |
17 | Rack::Response.new("OK", 201).finish
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/janky/notifier/chat_service.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Notifier
3 | class ChatService
4 | def self.completed(build)
5 | status = build.green? ? "was successful" : "failed"
6 | color = build.green? ? "green" : "red"
7 |
8 | message = "Build #%s (%s) of %s/%s %s (%ss) %s" % [
9 | build.number,
10 | build.short_sha1,
11 | build.repo_name,
12 | build.branch_name,
13 | status,
14 | build.duration,
15 | build.web_url
16 | ]
17 |
18 | ::Janky::ChatService.speak(message, build.room_id, {:color => color, :build => build})
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/janky/github/commit.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | class Commit
4 | def initialize(sha1, url, message, author, time)
5 | @sha1 = sha1
6 | @url = url
7 | @message = message
8 | @author = author
9 | @time = time
10 | end
11 |
12 | attr_reader :sha1, :url, :message, :author
13 |
14 | def committed_at
15 | @time
16 | end
17 |
18 | def to_hash
19 | { :id => @sha1,
20 | :url => @url,
21 | :message => @message,
22 | :author => {:name => @author},
23 | :timestamp => @time }
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/janky/views/console.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Views
3 | class Console < Layout
4 | def repo_name
5 | @build.repo_name
6 | end
7 |
8 | def repo_path
9 | "#{root}/#{repo_name}"
10 | end
11 |
12 | def branch_name
13 | @build.branch_name
14 | end
15 |
16 | def branch_path
17 | "#{repo_path}/#{branch_name}"
18 | end
19 |
20 | def commit_url
21 | @build.commit_url
22 | end
23 |
24 | def commit_short_sha
25 | @build.short_sha1
26 | end
27 |
28 | def output
29 | @build.output
30 | end
31 |
32 | def jenkins_url
33 | @build.url
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/janky/commit.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class Commit < ActiveRecord::Base
3 | belongs_to :repository
4 | has_many :builds
5 |
6 | def last_build
7 | builds.last
8 | end
9 |
10 | def build!(user, room_id = nil, compare = nil)
11 | compare = repository.github_url("compare/#{sha1}^...#{sha1}")
12 |
13 | room_id = room_id.to_s
14 | if room_id.empty? || room_id == "0"
15 | room_id = repository.room_id
16 | end
17 |
18 | builds.create!(
19 | :compare => compare,
20 | :user => user,
21 | :commit => self,
22 | :room_id => room_id,
23 | :branch_id => repository.branch_for('master').id
24 | )
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/janky/templates/layout.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{title}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{{yield}}}
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/lib/janky/notifier/multi.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Notifier
3 | # Dispatches notifications to multiple notifiers.
4 | class Multi
5 | def initialize(notifiers)
6 | @notifiers = notifiers
7 | end
8 |
9 | def queued(build)
10 | @notifiers.each do |notifier|
11 | notifier.queued(build) if notifier.respond_to?(:queued)
12 | end
13 | end
14 |
15 | def started(build)
16 | @notifiers.each do |notifier|
17 | notifier.started(build) if notifier.respond_to?(:started)
18 | end
19 | end
20 |
21 | def completed(build)
22 | @notifiers.each do |notifier|
23 | notifier.completed(build) if notifier.respond_to?(:completed)
24 | end
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
2 | ENV["RACK_ENV"] ||= "development"
3 |
4 | require "janky"
5 |
6 | class ActiveRecord::ConnectionAdapters::Mysql2Adapter
7 | NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
8 | end
9 | Janky.setup(ENV)
10 | require "janky/tasks"
11 |
12 | task "db:seed" do
13 | if ENV["RACK_ENV"] != "development"
14 | fail "refusing to load seed data into non-development database"
15 | end
16 |
17 | dump = File.expand_path("../lib/janky/database/seed.dump.gz", __FILE__)
18 |
19 | Replicate::Loader.new do |loader|
20 | loader.log_to $stderr, false, false
21 | loader.read Zlib::GzipReader.open(dump)
22 | end
23 | end
24 |
25 | task :test do
26 | abort "Use script/test to run the test suite."
27 | end
28 | task :default => :test
29 |
--------------------------------------------------------------------------------
/test/github_status_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../test_helper", __FILE__)
2 |
3 | class GithubStatusTest < Test::Unit::TestCase
4 | def stub_build
5 | @stub_build = stub(:repo_nwo => "github/janky",
6 | :sha1 => "xxxx",
7 | :green? => true,
8 | :number => 1,
9 | :duration => 1,
10 | :repository => stub(:context => "ci/janky"),
11 | :web_url => "http://example.com/builds/1")
12 | end
13 |
14 | def setup
15 | # never allow any outgoing requests
16 | Net::HTTP.any_instance.stubs(:request)
17 | end
18 |
19 | test "sending successful status uses the right path" do
20 | post = stub_everything
21 | Net::HTTP::Post.expects(:new).with("/repos/github/janky/statuses/xxxx").returns(post)
22 | notifier = Janky::Notifier::GithubStatus.new("token", "http://example.com/")
23 | notifier.completed(stub_build)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/janky/chat_service/campfire.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module ChatService
3 | class Campfire
4 | def initialize(settings)
5 | account = settings["JANKY_CHAT_CAMPFIRE_ACCOUNT"]
6 | if account.nil? || account.empty?
7 | raise Error, "JANKY_CHAT_CAMPFIRE_ACCOUNT setting is required"
8 | end
9 |
10 | token = settings["JANKY_CHAT_CAMPFIRE_TOKEN"]
11 | if token.nil? || token.empty?
12 | raise Error, "JANKY_CHAT_CAMPFIRE_TOKEN setting is required"
13 | end
14 |
15 | Broach.settings = {
16 | "account" => account,
17 | "token" => token,
18 | "use_ssl" => true,
19 | }
20 | end
21 |
22 | def speak(message, room_id, opts={})
23 | Broach.speak(ChatService.room_name(room_id), message)
24 | end
25 |
26 | def rooms
27 | @rooms ||= Broach.rooms.map do |room|
28 | Room.new(room.id, room.name)
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/janky/chat_service/hipchat.rb:
--------------------------------------------------------------------------------
1 | require "hipchat"
2 |
3 | module Janky
4 | module ChatService
5 | class HipChat
6 | def initialize(settings)
7 | token = settings["JANKY_CHAT_HIPCHAT_TOKEN"]
8 | if token.nil? || token.empty?
9 | raise Error, "JANKY_CHAT_HIPCHAT_TOKEN setting is required"
10 | end
11 |
12 | @client = ::HipChat::Client.new(token)
13 | @from = settings["JANKY_CHAT_HIPCHAT_FROM"] || "CI"
14 | end
15 |
16 | def speak(message, room_id, options = {})
17 | default = {
18 | :color => "yellow",
19 | :message_format => "text"
20 | }
21 | options = default.merge(options)
22 | @client[room_id].send(@from, message, options)
23 | end
24 |
25 | def rooms
26 | @rooms ||= @client.rooms.map do |room|
27 | Room.new(room.room_id, room.name)
28 | end
29 | end
30 | end
31 | end
32 |
33 | register_chat_service "hipchat", ChatService::HipChat
34 | end
35 |
--------------------------------------------------------------------------------
/lib/janky/tasks.rb:
--------------------------------------------------------------------------------
1 | require "rake"
2 | require "rake/tasklib"
3 |
4 | module Janky
5 | module Tasks
6 | extend Rake::DSL
7 |
8 | namespace :db do
9 | desc "Run the migration(s)"
10 | task :migrate do
11 | path = db_dir.join("migrate").to_s
12 | ActiveRecord::Migration.verbose = true
13 | ActiveRecord::Migrator.migrate(path)
14 |
15 | Rake::Task["db:schema:dump"].invoke
16 | end
17 |
18 | namespace :schema do
19 | desc "Dump the database schema into a standard Rails schema.rb file"
20 | task :dump do
21 | require "active_record/schema_dumper"
22 |
23 | path = db_dir.join("schema.rb").to_s
24 |
25 | File.open(path, "w:utf-8") do |fd|
26 | ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, fd)
27 | end
28 | end
29 | end
30 | end
31 |
32 | def self.db_dir
33 | @db_dir ||= Pathname(__FILE__).expand_path.join("../database")
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/janky/github/payload.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | class Payload
4 | def self.parse(json)
5 | parsed = PayloadParser.new(json)
6 | new(parsed.uri, parsed.branch, parsed.head, parsed.pusher,
7 | parsed.commits,
8 | parsed.compare)
9 | end
10 |
11 | def initialize(uri, branch, head, pusher, commits, compare)
12 | @uri = uri
13 | @branch = branch
14 | @head = head
15 | @pusher = pusher
16 | @commits = commits
17 | @compare = compare
18 | end
19 |
20 | attr_reader :uri, :branch, :head, :pusher, :commits, :compare
21 |
22 | def head_commit
23 | @commits.detect do |commit|
24 | commit.sha1 == @head
25 | end
26 | end
27 |
28 | def to_json
29 | { :after => @head,
30 | :ref => "refs/heads/#{@branch}",
31 | :pusher => {:name => @pusher},
32 | :uri => @uri,
33 | :commits => @commits,
34 | :compare => @compare }.to_json
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2012 GitHub, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/janky/builder/mock.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Builder
3 | class Mock
4 | def initialize(green, app)
5 | @green = green
6 | @app = app
7 | @builds = []
8 | end
9 |
10 | def run(params, create_url)
11 | params = Yajl.load(params)["parameter"]
12 | param = params.detect{ |p| p["name"] == "JANKY_ID" }
13 | build_id = param["value"]
14 | url = create_url.to_s.gsub("build", build_id.to_s)
15 |
16 | @builds << [build_id, "#{url}/", @green]
17 | end
18 |
19 | def output(build)
20 | "....FFFUUUUUUU"
21 | end
22 |
23 | def start
24 | @builds.each do |id, url, _|
25 | payload = Payload.start(id, url)
26 | request(payload)
27 | end
28 | end
29 |
30 | def complete
31 | @builds.each do |id, _, green|
32 | payload = Payload.complete(id, green)
33 | request(payload)
34 | end
35 | @builds.clear
36 | end
37 |
38 | def request(payload)
39 | Rack::MockRequest.new(@app).post("/_builder",
40 | :input => payload.to_json
41 | )
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/janky/builder/runner.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Builder
3 | class Runner
4 | def initialize(base_url, build, adapter)
5 | @base_url = base_url
6 | @build = build
7 | @adapter = adapter
8 | end
9 |
10 | def run
11 | context_push
12 | @adapter.run(json_params, create_url)
13 | end
14 |
15 | def output
16 | context_push
17 | @adapter.output(output_url)
18 | end
19 |
20 | def json_params
21 | Yajl.dump(:parameter => [
22 | { :name => "JANKY_SHA1", :value => @build.sha1 },
23 | { :name => "JANKY_BRANCH", :value => @build.branch_name },
24 | { :name => "JANKY_ID", :value => @build.id }
25 | ])
26 | end
27 |
28 | def output_url
29 | URI(@build.url + "consoleText")
30 | end
31 |
32 | def create_url
33 | URI.join(@base_url, "job/#{@build.repo_job_name}/build")
34 | end
35 |
36 | def context_push
37 | Exception.push(
38 | :base_url => @base_url.inspect,
39 | :build => @build.inspect,
40 | :adapter => @adapter.inspect,
41 | :params => json_params.inspect,
42 | :create_url => create_url.inspect
43 | )
44 | end
45 | end
46 | end
47 | end
48 |
49 |
--------------------------------------------------------------------------------
/lib/janky/exception.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Exception
3 | def self.setup(notifier)
4 | @notifier = notifier
5 | end
6 |
7 | def self.report(exception, context={})
8 | @notifier.report(exception, context)
9 | end
10 |
11 | def self.push(context)
12 | @notifier.push(context)
13 | end
14 |
15 | def self.reset!
16 | @notifier.reset!
17 | end
18 |
19 | def self.push_http_response(response)
20 | push(
21 | :response_code => response.code.inspect,
22 | :response_body => response.body.inspect
23 | )
24 | end
25 |
26 | class Logger
27 | def initialize(stream)
28 | @stream = stream
29 | @context = {}
30 | end
31 |
32 | def reset!
33 | @context = {}
34 | end
35 |
36 | def report(e, context={})
37 | @stream.puts "ERROR: #{e.class} - #{e.message}\n"
38 | @context.each do |k, v|
39 | @stream.puts "%12s %4s\n" % [k, v]
40 | end
41 | @stream.puts "\n#{e.backtrace.join("\n")}"
42 | end
43 |
44 | def push(context)
45 | @context.update(context)
46 | end
47 | end
48 |
49 | class Mock
50 | def self.push(context)
51 | end
52 |
53 | def self.report(e, context={})
54 | end
55 |
56 | def self.reset!
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/janky/github/payload_parser.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | class PayloadParser
4 | def initialize(json)
5 | @payload = Yajl.load(json)
6 | end
7 |
8 | def pusher
9 | @payload["pusher"]["name"]
10 | end
11 |
12 | def head
13 | @payload["after"]
14 | end
15 |
16 | def compare
17 | @payload["compare"]
18 | end
19 |
20 | def commits
21 | @payload["commits"].map do |commit|
22 | GitHub::Commit.new(
23 | commit["id"],
24 | commit["url"],
25 | commit["message"],
26 | normalize_author(commit["author"]),
27 | commit["timestamp"]
28 | )
29 | end
30 | end
31 |
32 | def normalize_author(author)
33 | if email = author["email"]
34 | "#{author["name"]} <#{email}>"
35 | else
36 | author
37 | end
38 | end
39 |
40 | def uri
41 | if uri = @payload["uri"]
42 | return uri
43 | end
44 |
45 | repository = @payload["repository"]
46 |
47 | if repository["private"]
48 | "git@#{GitHub.git_host}:#{URI(repository["url"]).path[1..-1]}"
49 | else
50 | uri = URI(repository["url"])
51 | uri.scheme = "git"
52 | uri.to_s
53 | end
54 | end
55 |
56 | def branch
57 | @payload["ref"].split("refs/heads/").last
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/janky/builder/http.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Builder
3 | class HTTP
4 | def initialize(username, password)
5 | @username = username
6 | @password = password
7 | end
8 |
9 | def run(params, create_url)
10 | http = Net::HTTP.new(create_url.host, create_url.port)
11 | if create_url.scheme == "https"
12 | http.use_ssl = true
13 | end
14 |
15 | request = Net::HTTP::Post.new(create_url.path)
16 | if @username && @password
17 | request.basic_auth(@username, @password)
18 | end
19 | request.form_data = {"json" => params}
20 |
21 | response = http.request(request)
22 |
23 | if !%w[302 201].include?(response.code)
24 | Exception.push_http_response(response)
25 | raise Error, "Failed to create build"
26 | end
27 | end
28 |
29 | def output(url)
30 | http = Net::HTTP.new(url.host, url.port)
31 | if url.scheme == "https"
32 | http.use_ssl = true
33 | end
34 |
35 | request = Net::HTTP::Get.new(url.path)
36 | if @username && @password
37 | request.basic_auth(@username, @password)
38 | end
39 |
40 | response = http.request(request)
41 |
42 | unless response.code == "200"
43 | Exception.push_http_response(response)
44 | raise Error, "Failed to get build output"
45 | end
46 |
47 | response.body
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/janky/build_request.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class BuildRequest
3 | def self.handle(repo_uri, branch_name, pusher, commit, compare, room_id)
4 | repos = Repository.where(uri: repo_uri)
5 | repos.each do |repo|
6 | begin
7 | new(repo, branch_name, pusher, commit, compare, room_id).handle
8 | rescue Janky::Error => boom
9 | Exception.report(boom, :repo => repo.name)
10 | end
11 | end
12 |
13 | repos.size
14 | end
15 |
16 | def initialize(repo, branch_name, pusher, commit, compare, room_id)
17 | @repo = repo
18 | @branch_name = branch_name
19 | @pusher = pusher
20 | @commit = commit
21 | @compare = compare
22 | @room_id = room_id
23 | end
24 |
25 | def handle
26 | current_build = commit.last_build
27 | build = branch.build_for(commit, @pusher, @room_id, @compare)
28 |
29 | if !current_build || (current_build && current_build.red?)
30 | if @repo.enabled?
31 | build.run
32 | Notifier.queued(build)
33 | end
34 | end
35 | end
36 |
37 | def branch
38 | @repo.branch_for(@branch_name)
39 | end
40 |
41 | def commit
42 | @repo.commit_for(
43 | :sha1 => @commit.sha1,
44 | :url => @commit.url,
45 | :message => @commit.message,
46 | :author => @commit.author,
47 | :committed_at => @commit.committed_at
48 | )
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/janky/notifier/mock.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Notifier
3 | # Mock notifier implementation used in testing environments.
4 | class Mock
5 | def initialize
6 | @notifications = []
7 | end
8 |
9 | attr_reader :notifications
10 |
11 | def queued(build)
12 | end
13 |
14 | def reset!
15 | @notifications.clear
16 | end
17 |
18 | def started(build)
19 | end
20 |
21 | def completed(build)
22 | notify(:completed, build)
23 | end
24 |
25 | def notify(state, build)
26 | @notifications << [state, build]
27 | end
28 |
29 | def success?(repo, branch, room_name)
30 | room_name ||= Janky::ChatService.default_room_name
31 |
32 | builds = @notifications.select do |state, build|
33 | state == :completed &&
34 | build.green? &&
35 | build.repo_name == repo &&
36 | build.branch_name == branch &&
37 | build.room_name == room_name
38 | end
39 |
40 | builds.size == 1
41 | end
42 |
43 | def failure?(repo, branch, room_name)
44 | room_name ||= Janky::ChatService.default_room_name
45 |
46 | builds = @notifications.select do |state, build|
47 | state == :completed &&
48 | build.red? &&
49 | build.repo_name == repo &&
50 | build.branch_name == branch &&
51 | build.room_name == room_name
52 | end
53 |
54 | builds.size == 1
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/janky/views/index.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | module Janky
3 | module Views
4 | class Index < Layout
5 | def jobs
6 | @builds.collect do |build|
7 | {
8 | :console_path => "/#{build.number}/output",
9 | :compare_url => build.compare,
10 | :repo_path => "/#{build.repo_name}",
11 | :branch_path => "/#{build.repo_name}/#{build.branch_name}",
12 | :repo_name => build.repo_name,
13 | :branch_name => build.branch_name,
14 | :status => css_status_for(build),
15 | :last_built_text => last_built_text_for(build),
16 | :message => build.commit_message,
17 | :sha1 => build.short_sha1,
18 | :author => build.commit_author.split("<").first
19 | }
20 | end
21 | end
22 |
23 | def css_status_for(build)
24 | if build.green?
25 | "good"
26 | elsif build.building?
27 | "building"
28 | elsif build.pending?
29 | "pending"
30 | elsif build.red?
31 | "janky"
32 | end
33 | end
34 |
35 | def last_built_text_for(build)
36 | if build.building?
37 | "Build started #{build.started_at}…"
38 | elsif build.completed?
39 | "Built in #{build.duration} seconds"
40 | elsif build.pending?
41 | "Build queued #{build.queued_at}…"
42 | end
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/janky/builder/payload.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Builder
3 | class Payload
4 | def self.parse(json, base_url)
5 | parsed = Yajl.load(json)
6 | build = parsed["build"]
7 |
8 | full_url = build["full_url"]
9 | path = build["url"]
10 | build_url = full_url || "#{base_url}#{path}"
11 |
12 | new(
13 | build["phase"],
14 | build["parameters"]["JANKY_ID"],
15 | build_url,
16 | build["status"]
17 | )
18 | end
19 |
20 | def self.start(id, url)
21 | new("STARTED", id, url, nil)
22 | end
23 |
24 | def self.complete(id, green)
25 | status = (green ? "SUCCESS" : "FAILED")
26 | new("FINISHED", id, nil, status)
27 | end
28 |
29 | def initialize(phase, id, url, status)
30 | @phase = phase
31 | @id = id
32 | @url = url
33 | @status = status
34 | end
35 |
36 | attr_reader :id, :url
37 |
38 | def started?
39 | @phase == "STARTED"
40 | end
41 |
42 | def completed?
43 | @phase == "FINISHED" || @phase == "FINALIZED"
44 | end
45 |
46 | def green?
47 | if completed?
48 | @status == "SUCCESS"
49 | else
50 | false
51 | end
52 | end
53 |
54 | def to_json
55 | { :build => {
56 | :phase => @phase,
57 | :status => @status,
58 | :full_url => @url,
59 | :parameters => {
60 | "JANKY_ID" => @id
61 | }
62 | }
63 | }.to_json
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/janky/database/migrate/1312115512_init.rb:
--------------------------------------------------------------------------------
1 | class Init < ActiveRecord::Migration
2 | def self.up
3 | create_table :repositories, :force => true do |t|
4 | t.string :name, :null => false
5 | t.string :uri, :null => false
6 | t.integer :room_id, :null => false
7 | t.timestamps
8 | end
9 | add_index :repositories, :name, :unique => true
10 | add_index :repositories, :uri, :unique => true
11 |
12 | create_table :branches, :force => true do |t|
13 | t.string :name, :null => false
14 | t.belongs_to :repository, :null => false
15 | t.timestamps
16 | end
17 | add_index :branches, [:name, :repository_id], :unique => true
18 |
19 | create_table :commits, :force => true do |t|
20 | t.string :sha1, :null => false
21 | t.string :message, :null => false
22 | t.string :author, :null => false
23 | t.datetime :committed_at
24 | t.belongs_to :repository, :null => false
25 | t.timestamps
26 | end
27 | add_index :commits, [:sha1, :repository_id], :unique => true
28 |
29 | create_table :builds, :force => true do |t|
30 | t.boolean :green, :default => false
31 | t.string :url, :null => true
32 | t.string :compare, :null => false
33 | t.datetime :started_at
34 | t.datetime :completed_at
35 | t.belongs_to :commit, :null => false
36 | t.belongs_to :branch, :null => false
37 | t.timestamps
38 | end
39 | add_index :builds, :url, :unique => true
40 | end
41 |
42 | def self.down
43 | drop_table :repositories
44 | drop_table :branches
45 | drop_table :commits
46 | drop_table :builds
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/janky/chat_service/hubot.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module ChatService
3 | class Hubot
4 | def initialize(settings)
5 | @available_rooms = settings["JANKY_CHAT_HUBOT_ROOMS"]
6 | @default_room = settings["JANKY_CHAT_HUBOT_DEFAULT_ROOM"]
7 | url = settings["JANKY_CHAT_HUBOT_URL"]
8 | if url.nil? || url.empty?
9 | raise Error, "JANKY_CHAT_HUBOT_URL setting is required"
10 | end
11 | @url = URI(url)
12 | end
13 |
14 | def speak(message, room, options = {:color => "yellow"})
15 | request(message, room)
16 | end
17 |
18 | def rooms
19 | @available_rooms.split(',').map do |room|
20 | id, name = room.strip.split(':')
21 | name ||= id
22 | Room.new(id, name)
23 | end
24 | end
25 |
26 | def request(message, room)
27 | room ||= @default_room
28 | uri = @url
29 | user = uri.user
30 | pass = uri.password
31 | path = File.join(uri.path, "janky")
32 |
33 | http = Net::HTTP.new(uri.host, uri.port)
34 | if uri.scheme == "https"
35 | http.use_ssl = true
36 | end
37 |
38 | post = Net::HTTP::Post.new(path)
39 | post.basic_auth(user, pass) if user && pass
40 | post["Content-Type"] = "application/json"
41 | post.body = {:message => message, :room => room}.to_json
42 | response = http.request(post)
43 | unless response.code == "200"
44 | Exception.push_http_response(response)
45 | raise Error, "Failed to notify"
46 | end
47 | end
48 | end
49 | end
50 |
51 | register_chat_service "hubot", ChatService::Hubot
52 | end
53 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | [fork]: https://github.com/github/janky/fork
4 | [pr]: https://github.com/github/janky/compare
5 | [style]: https://github.com/styleguide/ruby
6 | [code-of-conduct]: CODE_OF_CONDUCT.md
7 |
8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
9 |
10 | Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md).
11 |
12 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
13 |
14 | ## Submitting a pull request
15 |
16 | 0. [Fork][fork] and clone the repository
17 | 0. Configure and install the dependencies: `script/bootstrap`
18 | 0. Make sure the tests pass on your machine: `rake`
19 | 0. Create a new branch: `git checkout -b my-branch-name`
20 | 0. Make your change, add tests, and make sure the tests still pass
21 | 0. Push to your fork and [submit a pull request][pr]
22 | 0. Pat your self on the back and wait for your pull request to be reviewed and merged.
23 |
24 | Here are a few things you can do that will increase the likelihood of your pull request being accepted:
25 |
26 | - Follow the [style guide][style].
27 | - Write tests.
28 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
29 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
30 |
31 | ## Resources
32 |
33 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
34 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
35 | - [GitHub Help](https://help.github.com)
36 |
--------------------------------------------------------------------------------
/lib/janky/app.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class App < Sinatra::Base
3 | register Mustache::Sinatra
4 | register Helpers
5 |
6 | set :app_file, __FILE__
7 | enable :static
8 |
9 | set :mustache, {
10 | :namespace => Janky,
11 | :views => File.join(root, "views"),
12 | :templates => File.join(root, "templates")
13 | }
14 |
15 | before do
16 | if organization = github_organization
17 | github_organization_authenticate!(organization)
18 | end
19 | end
20 |
21 | def github_organization
22 | settings.respond_to?(:github_organization) && settings.github_organization
23 | end
24 |
25 | def github_team_id
26 | settings.respond_to?(:github_team_id) && settings.github_team_id
27 | end
28 |
29 | def authorize_index
30 | if github_team_id
31 | github_team_authenticate!(github_team_id)
32 | end
33 | end
34 |
35 | def authorize_repo(repo)
36 | if team_id = (repo.github_team_id || github_team_id)
37 | github_team_authenticate!(team_id)
38 | end
39 | end
40 |
41 | get "/?" do
42 | authorize_index
43 | @builds = Build.queued.first(50)
44 | mustache :index
45 | end
46 |
47 | get "/:build_id/output" do |build_id|
48 | @build = Build.select(:output).find(build_id)
49 | authorize_repo(@build.repo)
50 | mustache :console, :layout => false
51 | end
52 |
53 | get "/:repo_name" do |repo_name|
54 | repo = find_repo(repo_name)
55 | authorize_repo(repo)
56 |
57 | @builds = repo.builds.queued.first(50)
58 | mustache :index
59 | end
60 |
61 | get %r{^(?!\/auth\/github\/callback)\/([-_\.0-9a-zA-Z]+)\/([-_\.a-zA-z0-9\/]+)} do |repo_name, branch|
62 | repo = find_repo(repo_name)
63 | authorize_repo(repo)
64 |
65 | @builds = repo.branch_for(branch).queued_builds.first(50)
66 | mustache :index
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/janky/github/mock.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | class Mock
4 | Response = Struct.new(:code, :body)
5 |
6 | def initialize(user, password)
7 | @repos = {}
8 | @branch_shas = {}
9 | end
10 |
11 | def make_private(nwo)
12 | @repos[nwo] = :private
13 | end
14 |
15 | def make_public(nwo)
16 | @repos[nwo] = :public
17 | end
18 |
19 | def make_unauthorized(nwo)
20 | @repos[nwo] = :unauthorized
21 | end
22 |
23 | def set_branch_head(nwo, branch, sha)
24 | @branch_shas[[nwo, branch]] = sha
25 | end
26 |
27 | def create(nwo, secret, url)
28 | data = {"url" => "https://api.github.com/hooks/#{Time.now.to_f}"}
29 | Response.new("201", Yajl.dump(data))
30 | end
31 |
32 | def get(url)
33 | Response.new("200")
34 | end
35 |
36 | def delete(url)
37 | Response.new("204")
38 | end
39 |
40 | def repo_get(nwo)
41 | repo = {
42 | "name" => nwo.split("/").last,
43 | "private" => (@repos[nwo] == :private),
44 | "git_url" => "git://github.com/#{nwo}",
45 | "ssh_url" => "git@github.com:#{nwo}"
46 | }
47 |
48 | if @repos[nwo] == :unauthorized
49 | Response.new("404", Yajl.dump({}))
50 | else
51 | Response.new("200", Yajl.dump(repo))
52 | end
53 | end
54 |
55 | def branch(nwo, branch)
56 |
57 | data = { "sha" => @branch_shas[[nwo, branch]] }
58 |
59 | Response.new("200", Yajl.dump(data))
60 | end
61 |
62 | def commit(nwo, sha)
63 | data = {
64 | "commit" => {
65 | "author" => {
66 | "name" => "Test Author",
67 | "email" => "test@github.com"
68 | },
69 | "message" => "Test Message"
70 | }
71 | }
72 |
73 | Response.new("200", Yajl.dump(data))
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/janky/github/receiver.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | # Rack app handling GitHub Post-Receive [1] requests.
4 | #
5 | # The JSON payload is parsed into a GitHub::Payload. We then find the
6 | # associated Repository record based on the Payload's repository git URL
7 | # and create the associated records: Branch, Commit and Build.
8 | #
9 | # Finally, we trigger a new Jenkins build.
10 | #
11 | # [1]: http://help.github.com/post-receive-hooks/
12 | class Receiver
13 | def initialize(secret)
14 | @secret = secret
15 | end
16 |
17 | def call(env)
18 | dup.call!(env)
19 | end
20 |
21 | def call!(env)
22 | @request = Rack::Request.new(env)
23 |
24 | if !valid_signature?
25 | return Rack::Response.new("Invalid signature", 403).finish
26 | end
27 |
28 | if @request.content_type != "application/json"
29 | return Rack::Response.new("Invalid Content-Type", 400).finish
30 | end
31 |
32 | if !payload.head_commit
33 | return Rack::Response.new("Ignored", 400).finish
34 | end
35 |
36 | result = BuildRequest.handle(
37 | payload.uri,
38 | payload.branch,
39 | payload.pusher,
40 | payload.head_commit,
41 | payload.compare,
42 | @request.POST["room"]
43 | )
44 |
45 | Rack::Response.new("OK: #{result}", 201).finish
46 | end
47 |
48 | def valid_signature?
49 | digest = OpenSSL::Digest::SHA1.new
50 | signature = @request.env["HTTP_X_HUB_SIGNATURE"].split("=").last
51 |
52 | signature == OpenSSL::HMAC.hexdigest(digest, @secret, data)
53 | end
54 |
55 | def payload
56 | @payload ||= GitHub::Payload.parse(data)
57 | end
58 |
59 | def data
60 | @data ||= data!
61 | end
62 |
63 | def data!
64 | body = ""
65 | @request.body.each { |chunk| body << chunk }
66 | body
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/janky/chat_service/slack.rb:
--------------------------------------------------------------------------------
1 | require "slack"
2 |
3 | module Janky
4 | module ChatService
5 | class Slack
6 | def initialize(settings)
7 | team = settings["JANKY_CHAT_SLACK_TEAM"]
8 |
9 | if team.nil? || team.empty?
10 | raise Error, "JANKY_CHAT_SLACK_TEAM setting is required"
11 | end
12 |
13 | token = settings["JANKY_CHAT_SLACK_TOKEN"]
14 |
15 | if token.nil? || token.empty?
16 | raise Error, "JANKY_CHAT_SLACK_TOKEN setting is required"
17 | end
18 |
19 | username = settings["JANKY_CHAT_SLACK_USERNAME"] || 'CI'
20 | icon_url = settings["JANKY_CHAT_SLACK_ICON_URL"]
21 |
22 | @client = ::Slack::Client.new(team: team, token: token, username: username, icon_url: icon_url)
23 | end
24 |
25 | def speak(message, room_id, options = {})
26 | room_name = ChatService.room_name(room_id) || room_id
27 |
28 | if options[:build].present?
29 | @client.post_message(nil, room_name, {attachments: attachments(message, options[:build])})
30 | else
31 | @client.post_message(message, room_name, options)
32 | end
33 | end
34 |
35 | def rooms
36 | @rooms ||= @client.channels.map do |channel|
37 | Room.new(channel['id'], channel['name'])
38 | end
39 | end
40 |
41 | private
42 |
43 | def attachments(fallback, build)
44 | status = build.green? ? "was successful" : "failed"
45 | color = build.green? ? "good" : "danger"
46 |
47 | message = "Build #%s of %s/%s %s" % [
48 | build.number,
49 | build.repo_name,
50 | build.branch_name,
51 | status
52 | ]
53 |
54 | janky_field = ::Slack::AttachmentField.new("Janky", build.web_url, false)
55 | commit_field = ::Slack::AttachmentField.new("Commit", "<#{build.commit_url}|#{build.short_sha1}>", true)
56 | duration_field = ::Slack::AttachmentField.new("Duration", "#{build.duration}s", true)
57 | fields = [janky_field.to_h, commit_field.to_h, duration_field.to_h]
58 |
59 | [::Slack::Attachment.new(fallback, message, nil, color, ["text", "title", "fallback"], fields)]
60 | end
61 | end
62 | end
63 |
64 | register_chat_service "slack", ChatService::Slack
65 | end
66 |
--------------------------------------------------------------------------------
/lib/janky/builder/client.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Builder
3 | class Client
4 | def initialize(url, callback_url)
5 | @url = URI(url)
6 | @callback_url = URI(callback_url)
7 | end
8 |
9 | # The String absolute URL of the Jenkins server.
10 | attr_reader :url
11 |
12 | # The String absoulte URL callback of this Janky host.
13 | attr_reader :callback_url
14 |
15 | # Trigger a Jenkins build for the given Build.
16 | #
17 | # build - a Build object.
18 | #
19 | # Returns the Jenkins build URL.
20 | def run(build)
21 | Runner.new(@url, build, adapter).run
22 | end
23 |
24 | # Retrieve the output of the given Build.
25 | #
26 | # build - a Build object. Must have an url attribute.
27 | #
28 | # Returns the String build output.
29 | def output(build)
30 | Runner.new(@url, build, adapter).output
31 | end
32 |
33 | # Setup a job on the Jenkins server.
34 | #
35 | # name - The desired job name as a String.
36 | # repo_uri - The repository git URI as a String.
37 | # template_path - The Pathname to the XML config template.
38 | #
39 | # Returns nothing.
40 | def setup(name, repo_uri, template_path)
41 | job_creator.run(name, repo_uri, template_path)
42 | end
43 |
44 | # The adapter used to trigger builds. Defaults to HTTP, which hits the
45 | # Jenkins server configured by `setup`.
46 | def adapter
47 | @adapter ||= HTTP.new(url.user, url.password)
48 | end
49 |
50 | def job_creator
51 | @job_creator ||= JobCreator.new(url, @callback_url)
52 | end
53 |
54 | # Enable the mock adapter and make subsequent builds green.
55 | def green!
56 | @adapter = Mock.new(true, Janky.app)
57 | job_creator.enable_mock!
58 | end
59 |
60 | # Alias green! as enable_mock!
61 | alias_method :enable_mock!, :green!
62 |
63 | # Alias green! as reset!
64 | alias_method :reset!, :green!
65 |
66 | # Enable the mock adapter and make subsequent builds red.
67 | def red!
68 | @adapter = Mock.new(false, Janky.app)
69 | end
70 |
71 | # Simulate the first callback. Only available when mocked.
72 | def start!
73 | @adapter.start
74 | end
75 |
76 | # Simulate the last callback. Only available when mocked.
77 | def complete!
78 | @adapter.complete
79 | end
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/test/repository_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../test_helper", __FILE__)
2 |
3 | class RepositoryTest < Test::Unit::TestCase
4 | def setup
5 | Janky.setup(environment)
6 | Janky.enable_mock!
7 | Janky.reset!
8 |
9 | DatabaseCleaner.clean_with(:truncation)
10 | end
11 |
12 | test "job name includes repo name" do
13 | repo = Janky::Repository.setup("github/janky")
14 | assert_match /\Ajanky-.+/, repo.job_name
15 | end
16 |
17 | test "job name includes custom name" do
18 | repo = Janky::Repository.setup("github/janky", "janky2")
19 | assert_match /\Ajanky2-.+/, repo.job_name
20 | end
21 |
22 | test "job name includes truncated MD5 digest" do
23 | repo = Janky::Repository.setup("github/janky")
24 | assert_match /-[0-9a-f]{12}$/, repo.job_name
25 | end
26 |
27 | test "github owner is parsed correctly" do
28 | repo = Janky::Repository.setup("github/janky")
29 | assert_equal "github", repo.github_owner
30 | assert_equal "janky", repo.github_name
31 | end
32 |
33 | test "owner with a dash is parsed correctly" do
34 | repo = Janky::Repository.setup("digital-science/central-ftp-manage")
35 | assert_equal "digital-science", repo.github_owner
36 | assert_equal "central-ftp-manage", repo.github_name
37 | end
38 |
39 | test "repository with period is parsed correctly" do
40 | repo = Janky::Repository.setup("github/pygments.rb")
41 | assert_equal "github", repo.github_owner
42 | assert_equal "pygments.rb", repo.github_name
43 | end
44 |
45 | test "raises if there is no job config" do
46 | repo = Janky::Repository.setup("github/pygments.rb")
47 | # ensure we get file not found for job configs
48 | Janky.stubs(:jobs_config_dir).returns(Pathname("/tmp/"))
49 | assert_raise(Janky::Error) do
50 | puts repo.job_config_path
51 | repo.job_config_path
52 | end
53 | end
54 |
55 | test "default job config is selected if none provided" do
56 | repo = Janky::Repository.setup("github/pygments.rb", "pygments")
57 | assert_nil repo.job_template
58 | assert_match /default\.xml\.erb/, repo.job_config_path.to_s
59 | end
60 |
61 | test "custom job config is stored" do
62 | repo = Janky::Repository.setup("github/pygments.rb", "pygments", "custom")
63 | assert_equal "custom", repo.job_template
64 | end
65 |
66 | test "custom job config path is calculated" do
67 | repo = Janky::Repository.setup("github/pygments.rb", "pygments", "custom")
68 | assert_equal "custom", repo.job_template
69 | assert_match /custom\.xml\.erb/, repo.job_config_path.to_s
70 | end
71 |
72 | end
73 |
--------------------------------------------------------------------------------
/lib/janky/notifier.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Notifier
3 | # Setup the notifier.
4 | #
5 | # notifiers - One or more notifiers implementation to notify with.
6 | #
7 | # Returns nothing.
8 | def self.setup(notifiers)
9 | @adapter = Multi.new(Array(notifiers))
10 | end
11 |
12 | # Called whenever a build is queued
13 | #
14 | # build - the Build record.
15 | #
16 | # Returns nothing
17 | def self.queued(build)
18 | adapter.queued(build)
19 | end
20 |
21 | # Called whenever a build starts.
22 | #
23 | # build - the Build record.
24 | #
25 | # Returns nothing.
26 | def self.started(build)
27 | adapter.started(build)
28 | end
29 |
30 | # Called whenever a build completes.
31 | #
32 | # build - the Build record.
33 | #
34 | # Returns nothing.
35 | def self.completed(build)
36 | adapter.completed(build)
37 | end
38 |
39 | # The implementation used to send notifications.
40 | #
41 | # Returns a Multi instance by default or Mock when in mock mode.
42 | def self.adapter
43 | @adapter ||= Multi.new(@notifiers)
44 | end
45 |
46 | # Enable mocking. Once enabled, notifications are stored in a
47 | # in-memory Array exposed by the notifications method.
48 | #
49 | # Returns nothing.
50 | def self.enable_mock!
51 | @adapter = Mock.new
52 | end
53 |
54 | # Reset notification log. Only available when mocked. Typically called
55 | # before each test.
56 | #
57 | # Returns nothing.
58 | def self.reset!
59 | adapter.reset!
60 | end
61 |
62 | # Was any notification sent out? Only available when mocked.
63 | #
64 | # Returns a Boolean.
65 | def self.empty?
66 | notifications.empty?
67 | end
68 |
69 | # Was a success notification sent to the given room for the given
70 | # repo and branch?
71 | #
72 | # repo - the String repository name.
73 | # branch - the String branch name.
74 | # room - the optional String Campfire room slug.
75 | #
76 | # Returns a boolean.
77 | def self.success?(repo, branch, room=nil)
78 | adapter.success?(repo, branch, room)
79 | end
80 |
81 | # Same as `success?` but for failed notifications.
82 | def self.failure?(repo, branch, room=nil)
83 | adapter.failure?(repo, branch, room)
84 | end
85 |
86 | # Access the notification log. Only available when mocked.
87 | #
88 | # Returns an Array of notified Builds.
89 | def self.notifications
90 | adapter.notifications
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/lib/janky/notifier/github_status.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module Notifier
3 | # Create GitHub Status updates for builds.
4 | #
5 | # Note that Statuses are immutable - so we create one for
6 | # "pending" status when a build starts, then create a new status for
7 | # "success" or "failure" when the build is complete.
8 | class GithubStatus
9 | # Initialize with an OAuth token to POST Statuses with
10 | def initialize(token, api_url, context = nil)
11 | @token = token
12 | @api_url = URI(api_url)
13 | @default_context = context
14 | end
15 |
16 | def context(build)
17 | repository_context(build.repository) || @default_context
18 | end
19 |
20 | def repository_context(repository)
21 | repository && repository.context
22 | end
23 |
24 | # Create a Pending Status for the Commit when it is queued.
25 | def queued(build)
26 | repo = build.repo_nwo
27 | path = "repos/#{repo}/statuses/#{build.sha1}"
28 |
29 | post(path, "pending", build.web_url, "Build ##{build.number} queued", context(build))
30 | end
31 |
32 | # Create a Pending Status for the Commit when it starts.
33 | def started(build)
34 | repo = build.repo_nwo
35 | path = "repos/#{repo}/statuses/#{build.sha1}"
36 |
37 | post(path, "pending", build.web_url, "Build ##{build.number} started", context(build))
38 | end
39 |
40 | # Create a Success or Failure Status for the Commit.
41 | def completed(build)
42 | repo = build.repo_nwo
43 | path = "repos/#{repo}/statuses/#{build.sha1}"
44 | status = build.green? ? "success" : "failure"
45 |
46 | desc = case status
47 | when "success" then "Build ##{build.number} succeeded in #{build.duration}s"
48 | when "failure" then "Build ##{build.number} failed in #{build.duration}s"
49 | end
50 |
51 | post(path, status, build.web_url, desc, context(build))
52 | end
53 |
54 | # Internal: POST the new status to the API
55 | def post(path, status, url, desc, context = nil)
56 | http = Net::HTTP.new(@api_url.host, @api_url.port)
57 | post = Net::HTTP::Post.new("#{@api_url.path}#{path}")
58 |
59 | http.use_ssl = true
60 |
61 | post["Content-Type"] = "application/json"
62 | post["Authorization"] = "token #{@token}"
63 |
64 | body = {
65 | :state => status,
66 | :target_url => url,
67 | :description => desc,
68 | }
69 |
70 | unless context.nil?
71 | post["Accept"] = "application/vnd.github.she-hulk-preview+json"
72 | body[:context] = context
73 | end
74 |
75 | post.body = body.to_json
76 |
77 | http.request(post)
78 | end
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/janky/chat_service.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module ChatService
3 | Room = Struct.new(:id, :name)
4 |
5 | # Setup the adapter used to notify chat rooms of build status.
6 | #
7 | # name - Service name as a string.
8 | # settings - Service-specific setting hash.
9 | # default - Name of the default chat room as a String.
10 | #
11 | # Returns nothing.
12 | def self.setup(name, settings, default)
13 | klass = adapters[name]
14 |
15 | if !klass
16 | raise Error, "Unknown chat service: #{name.inspect}. Available " \
17 | "services are #{adapters.keys.join(", ")}"
18 | end
19 |
20 | @adapter = klass.new(settings)
21 | @default_room_name = default
22 | end
23 |
24 | class << self
25 | attr_accessor :adapter, :default_room_name
26 | end
27 |
28 | # Registry of available chat implementations.
29 | def self.adapters
30 | @adapters ||= {}
31 | end
32 |
33 | def self.default_room_id
34 | room_id(default_room_name)
35 | end
36 |
37 | # Send a message to a Chat room.
38 | #
39 | # message - The String message.
40 | # room_id - The String room ID.
41 | # options - Optional hash passed to the chat adapter.
42 | #
43 | # Returns nothing.
44 | def self.speak(message, room_id, options = {})
45 | adapter.speak(message, room_id, options)
46 | end
47 |
48 | # Get the ID of a room.
49 | #
50 | # slug - the String name of the room.
51 | #
52 | # Returns the room ID or nil for unknown rooms.
53 | def self.room_id(name)
54 | if room = rooms.detect { |room| room.name == name }
55 | room.id
56 | end
57 | end
58 |
59 | # Get the name of a room given its ID.
60 | #
61 | # id - the String room ID.
62 | #
63 | # Returns the name as a String or nil when not found.
64 | def self.room_name(id)
65 | if room = rooms.detect { |room| room.id.to_s == id.to_s }
66 | room.name
67 | end
68 | end
69 |
70 | # Get a list of all rooms names.
71 | #
72 | # Returns an Array of room name as Strings.
73 | def self.room_names
74 | rooms.map { |room| room.name }.sort
75 | end
76 |
77 | # Memoized list of available rooms.
78 | #
79 | # Returns an Array of Room objects.
80 | def self.rooms
81 | adapter.rooms
82 | end
83 |
84 | # Enable mocking. Once enabled, messages are discarded.
85 | #
86 | # Returns nothing.
87 | def self.enable_mock!
88 | @adapter = Mock.new
89 | end
90 |
91 | # Configure available rooms. Only available in mock mode.
92 | #
93 | # value - Hash of room map (String ID => String name)
94 | #
95 | # Returns nothing.
96 | def self.rooms=(value)
97 | adapter.rooms = value
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/janky/github/api.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | class API
4 | def initialize(url, user, password)
5 | @url = url
6 | @user = user
7 | @password = password
8 | end
9 |
10 | def create(nwo, secret, url)
11 | request = Net::HTTP::Post.new(build_path("repos/#{nwo}/hooks"))
12 | payload = build_payload(url, secret)
13 | request.body = Yajl.dump(payload)
14 | request.basic_auth(@user, @password)
15 |
16 | http.request(request)
17 | end
18 |
19 | def delete(hook_url)
20 | path = build_path(URI(hook_url).path)
21 | request = Net::HTTP::Delete.new(path)
22 | request.basic_auth(@user, @password)
23 |
24 | http.request(request)
25 | end
26 |
27 | def trigger(hook_url)
28 | path = build_path(URI(hook_url).path + "/test")
29 | request = Net::HTTP::Post.new(path)
30 | request.basic_auth(@user, @password)
31 |
32 | http.request(request)
33 | end
34 |
35 | def get(hook_url)
36 | path = build_path(URI(hook_url).path)
37 | request = Net::HTTP::Get.new(path)
38 | request.basic_auth(@user, @password)
39 |
40 | http.request(request)
41 | end
42 |
43 | def repo_get(nwo)
44 | path = build_path("repos/#{nwo}")
45 | request = Net::HTTP::Get.new(path)
46 | request.basic_auth(@user, @password)
47 |
48 | http.request(request)
49 | end
50 |
51 | def branch(nwo, branch)
52 | path = build_path("repos/#{nwo}/commits/#{branch}")
53 | request = Net::HTTP::Get.new(path)
54 | request.basic_auth(@user, @password)
55 |
56 | http.request(request)
57 | end
58 |
59 | def commit(nwo, sha)
60 | path = build_path("repos/#{nwo}/commits/#{sha}")
61 | request = Net::HTTP::Get.new(path)
62 | request.basic_auth(@user, @password)
63 |
64 | http.request(request)
65 | end
66 |
67 | def build_path(path)
68 | if path[0] == ?/
69 | URI.join(@url, path[1..-1]).path
70 | else
71 | URI.join(@url, path).path
72 | end
73 | end
74 |
75 | def build_payload(url, secret)
76 | { "name" => "web",
77 | "active" => true,
78 | "config" => {
79 | "url" => url,
80 | "secret" => secret,
81 | "content_type" => "json"
82 | }
83 | }
84 | end
85 |
86 | def http
87 | @http ||= http!
88 | end
89 |
90 | def http!
91 | uri = URI(@url)
92 | http = Net::HTTP.new(uri.host, uri.port)
93 |
94 | http.use_ssl = true
95 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER
96 | http.ca_path = "/etc/ssl/certs"
97 |
98 | http
99 | end
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/lib/janky/builder.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | # Triggers Jenkins builds and handles callbacks.
3 | #
4 | # The HTTP requests flow goes like this:
5 | #
6 | # 1. Send a Build request to the Jenkins server over HTTP. The resulting
7 | # build URL is stored in Build#url.
8 | #
9 | # 2. Once Jenkins picks up the build and starts running it, it sends a callback
10 | # handled by the `receiver` Rack app, which transitions the build into a
11 | # building state.
12 | #
13 | # 3. Finally, Jenkins sends another callback with the build result and the
14 | # build is transitioned to a completed and green/red state.
15 | #
16 | # The Mock adapter provides methods to simulate that flow without having to
17 | # go over the wire.
18 | module Builder
19 | # Set the callback URL of builder clients. Must be called before
20 | # registering any client.
21 | #
22 | # callback_url - The absolute callback URL as a String.
23 | #
24 | # Returns nothing.
25 | def self.setup(callback_url)
26 | @callback_url = callback_url
27 | end
28 |
29 | # Public: Define the rule for picking a builder.
30 | #
31 | # block - Required block that will be given a Repository object when
32 | # picking a builder. Must return a Client object.
33 | #
34 | # Returns nothing.
35 | def self.choose(&block)
36 | @chooser = block
37 | end
38 |
39 | # Pick the appropriate builder for a repo based on the rule set by the
40 | # choose method. Uses the default builder when no rule is defined.
41 | #
42 | # repo - a Repository object.
43 | #
44 | # Returns a Client object.
45 | def self.pick_for(repo)
46 | if block = @chooser
47 | block.call(repo)
48 | else
49 | self[:default]
50 | end
51 | end
52 |
53 | # Register a new build host.
54 | #
55 | # url - The String URL of the Jenkins server.
56 | #
57 | # Returns the new Client instance.
58 | def self.[]=(builder, url)
59 | builders[builder] = Client.new(url, @callback_url)
60 | end
61 |
62 | # Get the Client for a registered build host.
63 | #
64 | # builder - the String name of the build host.
65 | #
66 | # Returns the Client instance.
67 | def self.[](builder)
68 | builders[builder] ||
69 | raise(Error, "Unknown builder: #{builder.inspect}")
70 | end
71 |
72 | # Registered build hosts.
73 | #
74 | # Returns an Array of Client.
75 | def self.builders
76 | @builders ||= {}
77 | end
78 |
79 | # Rack app handling HTTP callbacks coming from the Jenkins server.
80 | def self.receiver
81 | @receiver ||= Janky::Builder::Receiver
82 | end
83 |
84 | def self.enable_mock!
85 | builders.values.each { |b| b.enable_mock! }
86 | end
87 |
88 | def self.green!
89 | builders.values.each { |b| b.green! }
90 | end
91 |
92 | def self.red!
93 | builders.values.each { |b| b.red! }
94 | end
95 |
96 | def self.reset!
97 | builders.values.each { |b| b.reset! }
98 | end
99 |
100 | def self.start!
101 | builders.values.each { |b| b.start! }
102 | end
103 |
104 | def self.complete!
105 | builders.values.each { |b| b.complete! }
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at codemattr@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
78 |
--------------------------------------------------------------------------------
/lib/janky/job_creator.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class JobCreator
3 | def initialize(server_url, callback_url)
4 | @server_url = server_url
5 | @callback_url = callback_url
6 | end
7 |
8 | def run(name, uri, template_path)
9 | creator.run(name, uri, template_path)
10 | end
11 |
12 | def creator
13 | @creator ||= Creator.new(HTTP, @server_url, @callback_url)
14 | end
15 |
16 | def enable_mock!
17 | @creator = Creator.new(Mock.new, @server_url, @callback_url)
18 | end
19 |
20 | class Creator
21 | def initialize(adapter, server_url, callback_url)
22 | @adapter = adapter
23 | @server_url = server_url
24 | @callback_url = callback_url
25 | end
26 |
27 | def run(name, uri, template_path)
28 | template = Tilt.new(template_path.to_s)
29 | config = template.render(Object.new, {
30 | :name => name,
31 | :repo => uri,
32 | :callback_url => @callback_url
33 | })
34 |
35 | exception_context(config, name, uri)
36 |
37 | if !@adapter.exists?(@server_url, name)
38 | @adapter.run(@server_url, name, config)
39 | true
40 | end
41 | end
42 |
43 | def exception_context(config, name, uri)
44 | Exception.push(
45 | :server_url => @server_url.inspect,
46 | :callback_url => @callback_url.inspect,
47 | :adapter => @adapter.inspect,
48 | :config => config.inspect,
49 | :name => name.inspect,
50 | :repo => uri.inspect
51 | )
52 | end
53 | end
54 |
55 | class Mock
56 | def run(server_url, name, config)
57 | name || raise(Error, "no name")
58 | config || raise(Error, "no config")
59 | (URI === server_url) || raise(Error, "server_url is not a URI")
60 |
61 | true
62 | end
63 |
64 | def exists?(server_url, name)
65 | false
66 | end
67 | end
68 |
69 | class HTTP
70 | def self.exists?(server_url, name)
71 | uri = server_url
72 | user = uri.user
73 | pass = uri.password
74 | path = uri.path
75 | http = Net::HTTP.new(uri.host, uri.port)
76 | if uri.scheme == "https"
77 | http.use_ssl = true
78 | end
79 |
80 | get = Net::HTTP::Get.new("#{path}/job/#{name}/")
81 | get.basic_auth(user, pass) if user && pass
82 | response = http.request(get)
83 |
84 | case response.code
85 | when "200"
86 | true
87 | when "404"
88 | false
89 | else
90 | Exception.push_http_response(response)
91 | raise "Failed to determine job existance"
92 | end
93 | end
94 |
95 | def self.run(server_url, name, config)
96 | uri = server_url
97 | user = uri.user
98 | pass = uri.password
99 | path = uri.path
100 | http = Net::HTTP.new(uri.host, uri.port)
101 | if uri.scheme == "https"
102 | http.use_ssl = true
103 | end
104 |
105 | post = Net::HTTP::Post.new("#{path}/createItem?name=#{name}")
106 | post.basic_auth(user, pass) if user && pass
107 | post["Content-Type"] = "application/xml"
108 | post.body = config
109 |
110 | response = http.request(post)
111 |
112 | unless response.code == "200"
113 | Exception.push_http_response(response)
114 | raise Error, "Failed to create job"
115 | end
116 | end
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/janky/database/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 1400144784) do
15 |
16 | create_table "branches", force: :cascade do |t|
17 | t.string "name", limit: 255, null: false
18 | t.integer "repository_id", limit: 4, null: false
19 | t.datetime "created_at"
20 | t.datetime "updated_at"
21 | end
22 |
23 | add_index "branches", ["name", "repository_id"], name: "index_branches_on_name_and_repository_id", unique: true, using: :btree
24 |
25 | create_table "builds", force: :cascade do |t|
26 | t.boolean "green", default: false
27 | t.string "url", limit: 255
28 | t.string "compare", limit: 255, null: false
29 | t.datetime "started_at"
30 | t.datetime "completed_at"
31 | t.integer "commit_id", limit: 4, null: false
32 | t.integer "branch_id", limit: 4, null: false
33 | t.datetime "created_at"
34 | t.datetime "updated_at"
35 | t.text "output", limit: 65535
36 | t.string "room_id", limit: 255
37 | t.string "user", limit: 255
38 | t.datetime "queued_at"
39 | end
40 |
41 | add_index "builds", ["branch_id"], name: "index_builds_on_branch_id", using: :btree
42 | add_index "builds", ["commit_id"], name: "index_builds_on_commit_id", using: :btree
43 | add_index "builds", ["completed_at"], name: "index_builds_on_completed_at", using: :btree
44 | add_index "builds", ["green"], name: "index_builds_on_green", using: :btree
45 | add_index "builds", ["started_at"], name: "index_builds_on_started_at", using: :btree
46 | add_index "builds", ["url"], name: "index_builds_on_url", unique: true, using: :btree
47 |
48 | create_table "commits", force: :cascade do |t|
49 | t.string "sha1", limit: 255, null: false
50 | t.text "message", limit: 65535, null: false
51 | t.string "author", limit: 255, null: false
52 | t.datetime "committed_at"
53 | t.integer "repository_id", limit: 4, null: false
54 | t.datetime "created_at"
55 | t.datetime "updated_at"
56 | t.string "url", limit: 255, null: false
57 | end
58 |
59 | add_index "commits", ["sha1", "repository_id"], name: "index_commits_on_sha1_and_repository_id", unique: true, using: :btree
60 |
61 | create_table "repositories", force: :cascade do |t|
62 | t.string "name", limit: 255, null: false
63 | t.string "uri", limit: 255, null: false
64 | t.string "room_id", limit: 255
65 | t.datetime "created_at"
66 | t.datetime "updated_at"
67 | t.boolean "enabled", default: true, null: false
68 | t.string "hook_url", limit: 255
69 | t.integer "github_team_id", limit: 4
70 | t.string "job_template", limit: 255
71 | t.string "context", limit: 255
72 | end
73 |
74 | add_index "repositories", ["enabled"], name: "index_repositories_on_enabled", using: :btree
75 | add_index "repositories", ["name"], name: "index_repositories_on_name", unique: true, using: :btree
76 | add_index "repositories", ["uri"], name: "index_repositories_on_uri", using: :btree
77 |
78 | end
79 |
--------------------------------------------------------------------------------
/lib/janky/branch.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class Branch < ActiveRecord::Base
3 | belongs_to :repository
4 | has_many :builds, :dependent => :destroy
5 |
6 | # Is this branch green?
7 | #
8 | # Returns a Boolean.
9 | def green?
10 | if current_build
11 | current_build.green?
12 | end
13 | end
14 |
15 | # Is this branch red?
16 | #
17 | # Returns a Boolean.
18 | def red?
19 | if current_build
20 | current_build.red?
21 | end
22 | end
23 |
24 | # Is this branch building?
25 | #
26 | # Returns a Boolean.
27 | def building?
28 | if current_build
29 | current_build.building?
30 | end
31 | end
32 |
33 | # Is this branch completed?
34 | #
35 | # Returns a Boolean.
36 | def completed?
37 | if current_build
38 | current_build.completed?
39 | end
40 | end
41 |
42 | # Find all completed builds, sorted by completion date, most recent first.
43 | #
44 | # Returns an Array of Builds.
45 | def completed_builds
46 | builds.completed
47 | end
48 |
49 | # See Build.queued.
50 | def queued_builds
51 | builds.queued
52 | end
53 |
54 | # Create a build for the given commit.
55 | #
56 | # commit - the Janky::Commit instance to build.
57 | # user - The login of the GitHub user who pushed.
58 | # compare - optional String GitHub Compare View URL. Defaults to the
59 | # commit last build, if any.
60 | # room_id - optional String room ID. Defaults to the room set on
61 | # the repository.
62 | #
63 | # Returns the newly created Janky::Build.
64 | def build_for(commit, user, room_id = nil, compare = nil)
65 | if compare.nil? && build = commit.last_build
66 | compare = build.compare
67 | end
68 |
69 | room_id = room_id.to_s
70 | if room_id.empty? || room_id == "0"
71 | room_id = repository.room_id
72 | end
73 |
74 | builds.create!(
75 | :compare => compare,
76 | :user => user,
77 | :commit => commit,
78 | :room_id => room_id
79 | )
80 | end
81 |
82 | # Fetch the HEAD commit of this branch using the GitHub API and create a
83 | # build and commit record.
84 | #
85 | # room_id - See build_for documentation. This is passed as is to the
86 | # build_for method.
87 | # user - Ditto.
88 | #
89 | # Returns the newly created Janky::Build.
90 | def head_build_for(room_id, user)
91 | sha_to_build = GitHub.branch_head_sha(repository.nwo, name)
92 | return if !sha_to_build
93 |
94 | commit = repository.commit_for_sha(sha_to_build)
95 |
96 | current_sha = current_build ? current_build.sha1 : "#{sha_to_build}^"
97 | compare_url = repository.github_url("compare/#{current_sha}...#{commit.sha1}")
98 | build_for(commit, user, room_id, compare_url)
99 | end
100 |
101 | # The current build, e.g. the most recent one.
102 | #
103 | # Returns a Build.
104 | def current_build
105 | builds.last
106 | end
107 |
108 | # Human readable status of this branch
109 | #
110 | # Returns a String.
111 | def status
112 | if current_build && current_build.building?
113 | "building"
114 | elsif build = completed_builds.first
115 | if build.green?
116 | "green"
117 | elsif build.red?
118 | "red"
119 | end
120 | elsif completed_builds.empty? || builds.empty?
121 | "no build"
122 | else
123 | raise Error, "unexpected branch status: #{id.inspect}"
124 | end
125 | end
126 |
127 | # Hash representation of this branch status.
128 | #
129 | # Returns a Hash with the name, status, sha1 and compare url.
130 | def to_hash
131 | {
132 | :name => repository.name,
133 | :status => status,
134 | :sha1 => (current_build && current_build.sha1),
135 | :compare => (current_build && current_build.compare)
136 | }
137 | end
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
2 |
3 | require "janky"
4 | require "test/unit"
5 | require "mocha/setup"
6 | require "database_cleaner"
7 |
8 | class Test::Unit::TestCase
9 | def self.test(name, &block)
10 | define_method("test_#{name.gsub(/\s+/,'_')}".to_sym, block)
11 | end
12 |
13 | def default_environment
14 | { "RACK_ENV" => "test",
15 | "JANKY_CONFIG_DIR" => File.dirname(__FILE__),
16 | "JANKY_GITHUB_USER" => "hubot",
17 | "JANKY_GITHUB_OAUTH_TOKEN" => "token",
18 | "JANKY_GITHUB_HOOK_SECRET" => "secret",
19 | "JANKY_HUBOT_USER" => "hubot",
20 | "JANKY_HUBOT_PASSWORD" => "password",
21 | "JANKY_CHAT_CAMPFIRE_ACCOUNT" => "github",
22 | "JANKY_CHAT_CAMPFIRE_TOKEN" => "token",
23 | "JANKY_CHAT_DEFAULT_ROOM" => "Builds",
24 | "JANKY_CHAT" => "campfire"
25 | }
26 | end
27 |
28 | def environment
29 | env = default_environment
30 | ENV.each do |key, value|
31 | if key =~ /^JANKY_/ || key == "DATABASE_URL"
32 | env[key] = value
33 | end
34 | end
35 | env
36 | end
37 |
38 | def gh_commit(sha1 = "HEAD")
39 | Janky::GitHub::Commit.new(
40 | sha1,
41 | "https://github.com/github/github/commit/#{sha1}",
42 | ":octocat:",
43 | "sr",
44 | Time.now
45 | )
46 | end
47 |
48 | def gh_payload(repo, branch, pusher, commits)
49 | head = commits.first
50 |
51 | Janky::GitHub::Payload.new(
52 | repo.uri,
53 | branch,
54 | head.sha1,
55 | pusher,
56 | commits,
57 | "http://github/compare/#{branch}...master"
58 | )
59 | end
60 |
61 | def get(path)
62 | Rack::MockRequest.new(Janky.app).get(path)
63 | end
64 |
65 | def gh_post_receive(repo_name, branch = "master", commit = "HEAD",
66 | pusher = "user")
67 |
68 | repo = Janky::Repository.find_by_name!(repo_name)
69 | payload = gh_payload(repo, branch, pusher, [gh_commit(commit)])
70 | digest = OpenSSL::Digest::SHA1.new
71 | sig = OpenSSL::HMAC.hexdigest(digest, Janky::GitHub.secret,
72 | payload.to_json)
73 |
74 | Janky::GitHub.set_branch_head(repo.nwo, branch, commit)
75 |
76 | Rack::MockRequest.new(Janky.app).post("/_github",
77 | :input => payload.to_json,
78 | "CONTENT_TYPE" => "application/json",
79 | "HTTP_X_HUB_SIGNATURE" => "sha1=#{sig}"
80 | )
81 | end
82 |
83 | def hubot_setup(nwo, name = nil)
84 | hubot_request("POST", "/_hubot/setup", :params => {
85 | :nwo => nwo,
86 | :name => name
87 | })
88 | end
89 |
90 | def hubot_build(repo, branch, room_name = nil, user = nil)
91 | params =
92 | if room_id = Janky::ChatService.room_id(room_name)
93 | {"room_id" => room_id.to_s}
94 | else
95 | {}
96 | end
97 |
98 | if user
99 | params["user"] = user
100 | end
101 |
102 | hubot_request("POST", "/_hubot/#{repo}/#{branch}", :params => params)
103 | end
104 |
105 | def hubot_status(repo=nil, branch=nil)
106 | if repo && branch
107 | hubot_request("GET", "/_hubot/#{repo}/#{branch}")
108 | else
109 | hubot_request("GET", "/_hubot")
110 | end
111 | end
112 |
113 | def hubot_last(options = {})
114 | hubot_request "GET",
115 | "/_hubot/builds?limit=#{options[:limit]}&building=#{options[:building]}"
116 | end
117 |
118 | def hubot_latest_build_sha(repo, branch)
119 | response = hubot_status(repo, branch)
120 | Yajl.load(response.body).first["sha1"]
121 | end
122 |
123 | def hubot_request(method, path, opts={})
124 | auth = ["#{Janky::Hubot.username}:#{Janky::Hubot.password}"].pack("m*")
125 | env = {"HTTP_AUTHORIZATION" => "Basic #{auth}"}
126 |
127 | Rack::MockRequest.new(Janky.app).request(method, path, env.merge(opts))
128 | end
129 |
130 | def hubot_toggle(repo)
131 | hubot_request("POST", "/_hubot/toggle/#{repo}")
132 | end
133 |
134 | def hubot_update_room(repo, room_name)
135 | hubot_request("PUT", "/_hubot/#{repo}", :params => {
136 | :room => room_name
137 | })
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/lib/janky/public/javascripts/jquery.relatize.js:
--------------------------------------------------------------------------------
1 | // jQuery Port of Rick Olson's relatize date Prototype plugin
2 | (function($) {
3 | $.fn.relatize = function() {
4 | return $(this).each(function() {
5 | if ($(this).hasClass( 'relatized' )) return
6 | $(this).text( $.relatize(this) ).addClass( 'relatized' )
7 | })
8 | }
9 |
10 | $.relatize = function(element) {
11 | var dateStr = $(element).text()
12 | var dateObj = new Date(dateStr)
13 | if (isNaN(dateObj)){
14 | // Rails outputs something like Thu Nov 12 16:00:33 -0800 2009
15 | // IE7 can't parse this, it wants something like
16 | // Thu Nov 12 2009 16:00:33 -0800
17 | var regex = /(\d\d:\d\d:\d\d [+-]\d{4}) (\d{4})$/
18 | dateObj = new Date(dateStr.replace(regex, "$2 $1"))
19 | if (isNaN(dateObj)){
20 | return dateStr;
21 | }
22 | }
23 | return $.relatize.timeAgoInWords(dateObj)
24 | }
25 |
26 | // shortcut
27 | var $r = $.relatize
28 |
29 | $.extend($.relatize, {
30 | shortDays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
31 | days: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
32 | 'Friday', 'Saturday' ],
33 | shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
34 | 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
35 | months: [ 'January', 'February', 'March', 'April', 'May', 'June',
36 | 'July', 'August', 'September', 'October', 'November',
37 | 'December' ],
38 |
39 | /**
40 | * Given a formatted string, replace the necessary items and return.
41 | * Example: Time.now().strftime("%B %d, %Y") => February 11, 2008
42 | * @param {String} format The formatted string used to format the results
43 | */
44 | strftime: function(date, format) {
45 | var day = date.getDay(), month = date.getMonth();
46 | var hours = date.getHours(), minutes = date.getMinutes();
47 |
48 | var pad = function(num) {
49 | var string = num.toString(10);
50 | return new Array((2 - string.length) + 1).join('0') + string
51 | };
52 |
53 | return format.replace(/\%([aAbBcdHImMpSwyY])/g, function(part) {
54 | switch(part.substr(1, 1)) {
55 | case 'a': return $r.shortDays[day]; break;
56 | case 'A': return $r.days[day]; break;
57 | case 'b': return $r.shortMonths[month]; break;
58 | case 'B': return $r.months[month]; break;
59 | case 'c': return date.toString(); break;
60 | case 'd': return pad(date.getDate()); break;
61 | case 'H': return pad(hours); break;
62 | case 'I': return pad((hours + 12) % 12); break;
63 | case 'm': return pad(month + 1); break;
64 | case 'M': return pad(minutes); break;
65 | case 'p': return hours > 12 ? 'PM' : 'AM'; break;
66 | case 'S': return pad(date.getSeconds()); break;
67 | case 'w': return day; break;
68 | case 'y': return pad(date.getFullYear() % 100); break;
69 | case 'Y': return date.getFullYear().toString(); break;
70 | }
71 | })
72 | },
73 |
74 | timeAgoInWords: function(targetDate, includeTime) {
75 | return $r.distanceOfTimeInWords(targetDate, new Date(), includeTime);
76 | },
77 |
78 | /**
79 | * Return the distance of time in words between two Date's
80 | * Example: '5 days ago', 'about an hour ago'
81 | * @param {Date} fromTime The start date to use in the calculation
82 | * @param {Date} toTime The end date to use in the calculation
83 | * @param {Boolean} Include the time in the output
84 | */
85 | distanceOfTimeInWords: function(fromTime, toTime, includeTime) {
86 | var delta = parseInt((toTime.getTime() - fromTime.getTime()) / 1000);
87 | if (delta < 60) {
88 | return 'just now';
89 | } else if (delta < 120) {
90 | return 'about a minute ago';
91 | } else if (delta < (45*60)) {
92 | return (parseInt(delta / 60)).toString() + ' minutes ago';
93 | } else if (delta < (120*60)) {
94 | return 'about an hour ago';
95 | } else if (delta < (24*60*60)) {
96 | return 'about ' + (parseInt(delta / 3600)).toString() + ' hours ago';
97 | } else if (delta < (48*60*60)) {
98 | return '1 day ago';
99 | } else {
100 | var days = (parseInt(delta / 86400)).toString();
101 | if (days > 5) {
102 | var fmt = '%B %d, %Y'
103 | if (includeTime) fmt += ' %I:%M %p'
104 | return $r.strftime(fromTime, fmt);
105 | } else {
106 | return days + " days ago"
107 | }
108 | }
109 | }
110 | })
111 | })(jQuery);
112 |
--------------------------------------------------------------------------------
/janky.gemspec:
--------------------------------------------------------------------------------
1 | require File.expand_path("../lib/janky/version", __FILE__)
2 |
3 | Gem::Specification.new do |s|
4 | s.name = "janky"
5 | s.version = Janky::VERSION
6 | s.description = "Janky is a Continuous Integration server"
7 | s.summary = "Continuous Integration server built on top of Jenkins and " \
8 | "designed for GitHub and Hubot"
9 | s.authors = ["Simon Rozet", "Matt Rogers"]
10 | s.email = 'codemattr@gmail.com'
11 | s.homepage = "https://github.com/github/janky"
12 | s.has_rdoc = false
13 | s.license = "MIT"
14 |
15 | s.post_install_message = <<-EOL
16 | If you are upgrading from Janky 0.9.13, you will want to add a JANKY_BRANCH parameter
17 | to your config/default.xml.erb. See
18 | https://github.com/github/janky/commit/0fc6214e3a75cc138aed46a2493980440e848aa3#commitcomment-1815400 for details.
19 | EOL
20 |
21 | # runtime
22 | s.add_dependency "rake", "~>12.0"
23 | s.add_dependency "sinatra", "~>1.3"
24 | s.add_dependency "sinatra_auth_github", "~>1.0.0"
25 | s.add_dependency "mustache", "~>0.11"
26 | s.add_dependency "yajl-ruby", "~>1.3.1"
27 | s.add_dependency "activerecord", "~>4.2.0"
28 | s.add_dependency "activerecord-deprecated_finders", "~>1.0.4"
29 | s.add_dependency "broach", "~>0.2"
30 | s.add_dependency "replicate", "~>1.4"
31 |
32 | # development
33 | s.add_development_dependency "shotgun", "~>0.9"
34 | s.add_development_dependency "thin", "~>1.2"
35 | s.add_development_dependency "mysql2", "~>0.3.0"
36 | s.add_development_dependency "test-unit", "~>3.2.0"
37 |
38 | # test
39 | s.add_development_dependency "database_cleaner", "1.6.2"
40 | s.add_development_dependency "mocha", "~>1.5.0"
41 |
42 | s.files = %w[
43 | CHANGES
44 | COPYING
45 | Gemfile
46 | README.md
47 | Rakefile
48 | config.ru
49 | janky.gemspec
50 | lib/janky.rb
51 | lib/janky/app.rb
52 | lib/janky/branch.rb
53 | lib/janky/build.rb
54 | lib/janky/build_request.rb
55 | lib/janky/builder.rb
56 | lib/janky/builder/client.rb
57 | lib/janky/builder/http.rb
58 | lib/janky/builder/mock.rb
59 | lib/janky/builder/payload.rb
60 | lib/janky/builder/receiver.rb
61 | lib/janky/builder/runner.rb
62 | lib/janky/chat_service.rb
63 | lib/janky/chat_service/campfire.rb
64 | lib/janky/chat_service/hipchat.rb
65 | lib/janky/chat_service/hubot.rb
66 | lib/janky/chat_service/slack.rb
67 | lib/janky/chat_service/mock.rb
68 | lib/janky/commit.rb
69 | lib/janky/database/migrate/1312115512_init.rb
70 | lib/janky/database/migrate/1312117285_non_unique_repo_uri.rb
71 | lib/janky/database/migrate/1312198807_repo_enabled.rb
72 | lib/janky/database/migrate/1313867551_add_build_output_column.rb
73 | lib/janky/database/migrate/1313871652_add_commit_url_column.rb
74 | lib/janky/database/migrate/1317384618_add_repo_hook_url.rb
75 | lib/janky/database/migrate/1317384619_add_build_room_id.rb
76 | lib/janky/database/migrate/1317384629_drop_default_room_id.rb
77 | lib/janky/database/migrate/1317384649_github_team_id.rb
78 | lib/janky/database/migrate/1317384650_add_build_indexes.rb
79 | lib/janky/database/migrate/1317384651_add_more_build_indexes.rb
80 | lib/janky/database/migrate/1317384652_change_commit_message_to_text.rb
81 | lib/janky/database/migrate/1317384653_add_build_pusher.rb
82 | lib/janky/database/migrate/1317384654_add_build_queued_at.rb
83 | lib/janky/database/migrate/1317384655_add_template.rb
84 | lib/janky/database/migrate/1398262033_add_context.rb
85 | lib/janky/database/migrate/1400144784_change_room_id_to_string.rb
86 | lib/janky/database/schema.rb
87 | lib/janky/database/seed.dump.gz
88 | lib/janky/exception.rb
89 | lib/janky/github.rb
90 | lib/janky/github/api.rb
91 | lib/janky/github/commit.rb
92 | lib/janky/github/mock.rb
93 | lib/janky/github/payload.rb
94 | lib/janky/github/payload_parser.rb
95 | lib/janky/github/receiver.rb
96 | lib/janky/helpers.rb
97 | lib/janky/hubot.rb
98 | lib/janky/job_creator.rb
99 | lib/janky/notifier.rb
100 | lib/janky/notifier/chat_service.rb
101 | lib/janky/notifier/failure_service.rb
102 | lib/janky/notifier/github_status.rb
103 | lib/janky/notifier/mock.rb
104 | lib/janky/notifier/multi.rb
105 | lib/janky/public/css/base.css
106 | lib/janky/public/images/building-bot.gif
107 | lib/janky/public/images/disclosure-arrow.png
108 | lib/janky/public/images/logo.png
109 | lib/janky/public/images/robawt-status.gif
110 | lib/janky/public/javascripts/application.js
111 | lib/janky/public/javascripts/jquery.js
112 | lib/janky/public/javascripts/jquery.relatize.js
113 | lib/janky/repository.rb
114 | lib/janky/tasks.rb
115 | lib/janky/templates/console.mustache
116 | lib/janky/templates/index.mustache
117 | lib/janky/templates/layout.mustache
118 | lib/janky/version.rb
119 | lib/janky/views/console.rb
120 | lib/janky/views/index.rb
121 | lib/janky/views/layout.rb
122 | ]
123 |
124 | s.test_files = %w[
125 | test/default.xml.erb
126 | test/janky_test.rb
127 | test/test_helper.rb
128 | ]
129 | end
130 |
--------------------------------------------------------------------------------
/CHANGES:
--------------------------------------------------------------------------------
1 | = 0.12.0 / 2017-06-05
2 | * Update yajl-ruby to work with Ruby 2.4
3 | * Add the ability to use MySQL 5.7
4 | * Update mocha to a more recent version
5 | * Allow DATABASE_URL to be specified for tests
6 |
7 | = 0.11.1 / 2015-02-20
8 | * Update gemspec to include the hubot chat service files
9 | * Update recommended Jenkins version in the README
10 |
11 | = 0.11.0 / 2014-11-14
12 |
13 | * Add support for Slack chat rooms
14 | * Convert chat room ids from integers to strings
15 | * Ensure build completion works with new Jenkins versions
16 | * Allow custom build templates to be provided when setting up projects
17 | * Mark builds that are queued in Jenkins as pending on GitHub
18 | * Delete and recreate hooks when setting up repositories
19 | * Add the ability to delete repos via `/ci delete`
20 | * Get detailed info about repos via `/ci show`
21 | * Send updates from Janky as a separate context to GitHub
22 |
23 | = 0.10.2 / 2013-10-02
24 |
25 | * Revert AR deprecation warnings
26 | * Revert previous configure jobs throwing error change
27 |
28 | = 0.10.1 / 2013-09-20
29 |
30 | * Refresh landing page every 5 seconds for updates
31 | * Previously configured jobs reconfigure instead of throwing errors
32 | * Add an API endpoint to get a json response full of build statuses
33 | * Fixup stupid AR deprecation warnings
34 |
35 | = 0.10.0 / 2013-09-19
36 |
37 | * Upgrade sinatra_auth_github to work with latest octokit
38 |
39 | = 0.9.15 / 2013-07-25
40 |
41 | * Upgrade sinatra_auth_github to make this work better with latest GitHub.com
42 | requirements
43 |
44 | * Truncate MD5 digests in job names to 12 characters
45 |
46 | = 0.9.14 / 2013-02-26
47 |
48 | * Many doc changes and improvements [Rob Sanheim]
49 |
50 | * Make the Jenkins job name human readable [Riley Guerin]
51 |
52 | * Added GithubStatus Notifier for GitHub Status API.
53 | See https://github.com/blog/1227-status-api [Rob Sainheim]
54 |
55 | * Added Build#queued_at for tracking when builds were queued vs actually
56 | built. [Simon Rozet]
57 |
58 | = 0.9.13 / 2012-06-24
59 |
60 | * Pull down the branch HEAD commit from the GitHub API for manual builds.
61 | This ensures that the latest code is built even when the webhook fails.
62 | [Jake Douglas, Simon Rozet]
63 |
64 | * Record the name of the user that triggered a build in the database. This
65 | is mostly useful for applications consuming the Janky API and isn't exposed
66 | anywhere in the UI. [Jake Douglas, Simon Rozet]
67 |
68 | * Use full SHA1s when interacting with other systems to avoid ambiguous
69 | commits. [Jake Douglas]
70 |
71 | * Deprecate support for Ruby < 1.9.3. [Simon Rozet]
72 |
73 | * Add missing database migrations that were wrongfully omitted from the
74 | 0.9.12 gem package. [Simon Rozet]
75 |
76 | = 0.9.12 / 2012-06-23
77 |
78 | * Upgrade OAuth authentication gem to use GitHub API v3. [Corey Donohoe]
79 |
80 | * Upgrade to ActiveRecord 3.2.X. [Simon Rozet]
81 |
82 | * Allow configuring the path to the database socket with JANKY_DATABASE_SOCKET
83 | setting. [Simon Rozet]
84 |
85 | * Add a few indexes on the builds table to improve performance. [Simon Rozet].
86 |
87 | * Eager load the builds.output column to improve performance. [Simon Rozet,
88 | Jesse Dearing, Rafael Mendonça França]
89 |
90 | * Fix deprecation logic for Campfire settings. [Piet Jaspers]
91 |
92 | * Destroy all associated records after destroying a repository record. [Eric
93 | Holmes]
94 |
95 | * Handle commit messages longer than 255 characters. [Shay Frendt]
96 |
97 | = 0.9.11 / 2011-03-01
98 |
99 | * Fix HipChat setup instructions. [Andre Sachs]
100 |
101 | * Fix OAuth authentication bug introduced in version 0.9.9. [Lucas Mazza]
102 |
103 | * Fix `db:migrate` Rake task on Heroku when using HipChat. See the updated
104 | gist at https://gist.github.com/1497335. [Simon Rozet]
105 |
106 | = 0.9.10 / 2011-02-11
107 |
108 | * Fix an issue where Campfire settings are overridden on Heroku [Simon Rozet]
109 |
110 | = 0.9.9 / 2011-02-11
111 |
112 | * HipChat support [Justin Smestad, Seth Chisamore, Simon Rozet]
113 |
114 | * Support for GitHub Enterprise. [Dusty Burwell, Simon Rozet]
115 |
116 | * Support for GitHub logins containing dashes. [Thom May]
117 |
118 | * Support for branches containing slashes in their name. [Andres Torres]
119 |
120 | * Respond with proper status code to invalid Jenkins notification
121 | requests. [Chris Mytton]
122 |
123 | * Support for Jenkins servers running behind SSL. [Vivek Pandey,
124 | Gavin Heavyside, Thom May]
125 |
126 | * Support for Jenkins servers running under a path [Piet Jaspers]
127 |
128 | * Deprecate JANKY_CAMPFIRE_ACCOUNT. Please use JANKY_CHAT_CAMPFIRE_ACCOUNT
129 | instead. [Simon Rozet]
130 |
131 | * Deprecate JANKY_CAMPFIRE_TOKEN. Please use JANKY_CHAT_CAMPFIRE_TOKEN
132 | instead. [Simon Rozet]
133 |
134 | * Deprecate JANKY_CAMPFIRE_DEFAULT_ROOM. Please use
135 | JANKY_CHAT_DEFAULT_ROOM instead. [Simon Rozet]
136 |
137 | * Both JANKY_BASE_URL and JANKY_DEFAULT_BUILDER now require a trailing
138 | slash. [Simon Rozet]
139 |
140 | = 0.9 / 2011-12-19
141 |
142 | * Initial public release.
143 |
--------------------------------------------------------------------------------
/lib/janky/public/css/base.css:
--------------------------------------------------------------------------------
1 | /*------------------------------------------------------------------------------------
2 | @group Global Reset
3 | ------------------------------------------------------------------------------------*/
4 | * {
5 | padding:0;
6 | margin:0;
7 | }
8 | h1, h2, h3, h4, h5, h6, p, pre, blockquote, label, ul, ol, dl, fieldset, address { margin:1em 0; }
9 | li, dd { margin-left:5%; }
10 | fieldset { padding: .5em; }
11 | select option{ padding:0 5px; }
12 |
13 | .access{ display:none; } /* For accessibility related elements */
14 | .clear{ clear:both; height:0px; font-size:0px; line-height:0px; overflow:hidden; }
15 | a{ outline:none; }
16 | a img{ border:none; }
17 |
18 | .clearfix:after {
19 | content: ".";
20 | display: block;
21 | height: 0;
22 | clear: both;
23 | visibility: hidden;
24 | }
25 | * html .clearfix {height: 1%;}
26 | .clearfix {display:inline-block;}
27 | .clearfix {display: block;}
28 | .right {float: right;}
29 |
30 | /* @end */
31 |
32 | /*----------------------------------------------------------------------------
33 | @group Base Layout
34 | ----------------------------------------------------------------------------*/
35 |
36 | body{
37 | margin:0;
38 | padding:0;
39 | font-size:14px;
40 | line-height:1.6;
41 | font-family:Helvetica, Arial, sans-serif;
42 | background:#fff;
43 | }
44 |
45 | #wrapper{
46 | margin:0 auto;
47 | width:600px;
48 | }
49 | .wide #wrapper{
50 | width:1000px;
51 | }
52 |
53 | h2#logo{
54 | width:600px;
55 | margin:0 auto 25px auto;
56 | }
57 | h2#logo a{
58 | display:block;
59 | height:156px;
60 | text-indent:-9999px;
61 | text-decoration:none;
62 | background:url(../images/logo.png);
63 | }
64 |
65 | .content{
66 | padding:5px;
67 | background:#ededed;
68 | border-radius:4px;
69 | margin-bottom: 50px;
70 | }
71 | .content > .inside{
72 | border:1px solid #ddd;
73 | background:#fff;
74 | border-radius:3px;
75 | }
76 |
77 | /* @end */
78 |
79 | /*----------------------------------------------------------------------------
80 | @group Builds
81 | ----------------------------------------------------------------------------*/
82 |
83 | ul.builds{
84 | margin:0;
85 | }
86 |
87 | ul.builds li{
88 | list-style-type:none;
89 | margin:0;
90 | padding:12px 10px;
91 | border-bottom:1px solid #e5e5e5;
92 | border-top:1px solid #fff;
93 | background:-webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f2f2f2));
94 | background:-moz-linear-gradient(top, #fdfdfd, #f2f2f2);
95 | }
96 | ul.builds li:first-child{
97 | border-top:none;
98 | border-top-left-radius: 3px;
99 | border-top-right-radius: 3px;
100 | }
101 | ul.builds li:last-child{
102 | border-bottom:none;
103 | border-bottom-left-radius: 3px;
104 | border-bottom-right-radius: 3px;
105 | }
106 | ul.builds li:hover{
107 | background:-webkit-gradient(linear, left top, left bottom, from(#f5f9fb), to(#e9eef0));
108 | background:-moz-linear-gradient(top, #f5f9fb, #e9eef0);
109 | }
110 | ul.builds li.building:hover{
111 | background:-webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f2f2f2));
112 | background:-moz-linear-gradient(top, #fdfdfd, #f2f2f2);
113 | }
114 |
115 | ul.builds a{
116 | text-decoration:none;
117 | }
118 |
119 | ul.builds a.console{
120 | float: right;
121 | display:block;
122 | width: 22px;
123 | height: 40px;
124 | margin-left: 10px;
125 | background:url(../images/disclosure-arrow.png) 65% 10px no-repeat;
126 | }
127 | ul.builds li:hover a.console{
128 | background-position:65% -90px;
129 | }
130 |
131 | ul.builds .status{
132 | float:left;
133 | margin-top:5px;
134 | margin-right:10px;
135 | width:37px;
136 | height:34px;
137 | background:url(../images/robawt-status.gif) 0 0 no-repeat;
138 | }
139 | ul.builds .building .status{
140 | background:url(../images/building-bot.gif);
141 | }
142 | ul.builds .janky .status{
143 | background-position:0 -200px;
144 | }
145 | ul.builds .pending .status{
146 | background-position:0 -100px;
147 | }
148 |
149 | ul.builds h2{
150 | margin:0;
151 | font-size:16px;
152 | text-shadow:0 1px #fff;
153 | }
154 | ul.builds h2 span{
155 | font-weight: normal;
156 | color: #666666;
157 | }
158 | ul.builds .good a{
159 | color:#358c00;
160 | }
161 | ul.builds .good h2{
162 | color:#358c00;
163 | }
164 | ul.builds .building a{
165 | color:#e59741;
166 | }
167 | ul.builds .building h2{
168 | color:#e59741;
169 | }
170 | ul.builds .pending a{
171 | color:#e59741;
172 | }
173 | ul.builds .pending h2{
174 | color:#e59741;
175 | }
176 | ul.builds .janky a{
177 | color:#ae0000;
178 | }
179 | ul.builds .janky h2{
180 | color:#ae0000;
181 | }
182 | ul.builds p.sha1{
183 | margin-top: 2px;
184 | }
185 | ul.builds p{
186 | margin:-2px 0 0 0;
187 | font-size:13px;
188 | font-weight:200;
189 | color:#666;
190 | text-shadow:0 1px #fff;
191 | }
192 | ul.builds .building p{
193 | color:#999;
194 | }
195 |
196 | /* @end */
197 |
198 | /*----------------------------------------------------------------------------
199 | @group Text Styles
200 | ----------------------------------------------------------------------------*/
201 |
202 | pre{
203 | margin:10px;
204 | font-size:12px;
205 | overflow:auto;
206 | }
207 | pre::-webkit-scrollbar {
208 | height: 8px;
209 | width: 8px;
210 | }
211 | pre::-webkit-scrollbar-track-piece{
212 | margin-bottom:10px;
213 | background-color: #e5e5e5;
214 | border-bottom-left-radius: 4px 4px;
215 | border-bottom-right-radius: 4px 4px;
216 | border-top-left-radius: 4px 4px;
217 | border-top-right-radius: 4px 4px;
218 | }
219 |
220 | pre::-webkit-scrollbar-thumb:vertical{
221 | height: 25px;
222 | background-color: #ccc;
223 | -webkit-border-radius: 4px;
224 | -webkit-box-shadow: 0 1px 1px rgba(255,255,255,1);
225 | }
226 | pre::-webkit-scrollbar-thumb:horizontal{
227 | width: 25px;
228 | background-color: #ccc;
229 | -webkit-border-radius: 4px;
230 | }
231 |
232 | /* @end */
233 |
--------------------------------------------------------------------------------
/lib/janky/github.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | module GitHub
3 | # Setup the GitHub API client and Post-Receive hook endpoint.
4 | #
5 | # user - API user as a String.
6 | # password - API password as a String.
7 | # secret - Secret used to sign hook requests from GitHub.
8 | # hook_url - String URL that handles Post-Receive requests.
9 | # api_url - GitHub API URL as a String. Requires a trailing slash.
10 | # git_host - Hostname where git repos are hosted. e.g. "github.com"
11 | #
12 | # Returns nothing.
13 | def self.setup(user, password, secret, hook_url, api_url, git_host)
14 | @user = user
15 | @password = password
16 | @secret = secret
17 | @hook_url = hook_url
18 | @api_url = api_url
19 | @git_host = git_host
20 | end
21 |
22 | class << self
23 | attr_reader :secret, :git_host
24 | end
25 |
26 | # URL of the GitHub website.
27 | #
28 | # Retuns the URL as a String. Example: https://github.com
29 | def self.github_url
30 | api_uri = URI.parse(@api_url)
31 | "#{api_uri.scheme}://#{@git_host}"
32 | end
33 |
34 | # Rack app that handles Post-Receive hook requests from GitHub.
35 | #
36 | # Returns a GitHub::Receiver.
37 | def self.receiver
38 | @receiver ||= Receiver.new(@secret)
39 | end
40 |
41 | # Fetch repository details.
42 | # http://developer.github.com/v3/repos/#get
43 | #
44 | # nwo - qualified "owner/repo" name.
45 | #
46 | # Returns the Hash representation of the repo, nil when it doesn't exists
47 | # or access was denied.
48 | # Raises an Error for any unexpected response.
49 | def self.repo_get(nwo)
50 | response = api.repo_get(nwo)
51 |
52 | case response.code
53 | when "200"
54 | Yajl.load(response.body)
55 | when "403", "404"
56 | nil
57 | else
58 | Exception.push_http_response(response)
59 | raise Error, "Failed to get hook"
60 | end
61 | end
62 |
63 | # Fetch the SHA1 of the given branch HEAD.
64 | #
65 | # nwo - qualified "owner/repo" name.
66 | # branch - Name of the branch as a String.
67 | #
68 | # Returns the SHA1 as a String or nil when the branch doesn't exists.
69 | def self.branch_head_sha(nwo, branch)
70 | response = api.branch(nwo, branch)
71 |
72 | branch = Yajl.load(response.body)
73 | branch && branch["sha"]
74 | end
75 |
76 | # Fetch commit details for the given SHA1.
77 | #
78 | # nwo - qualified "owner/repo" name.
79 | # sha - SHA1 of the commit as a String.
80 | #
81 | # Example
82 | #
83 | # commit("github/janky", "35fff49dc18376845dd37e785c1ea88c6133f928")
84 | # => { "commit" => {
85 | # "author" => {
86 | # "name" => "Simon Rozet",
87 | # "email" => "sr@github.com",
88 | # },
89 | # "message" => "document and clean up Branch#build_for_head",
90 | # }
91 | # }
92 | #
93 | # Returns the commit Hash.
94 | def self.commit(nwo, sha)
95 | response = api.commit(nwo, sha)
96 |
97 | if response.code != "200"
98 | Exception.push_http_response(response)
99 | raise Error, "Failed to get commit"
100 | end
101 |
102 | Yajl.load(response.body)
103 | end
104 |
105 | # Create a Post-Receive hook for the given repository.
106 | # http://developer.github.com/v3/repos/hooks/#create-a-hook
107 | #
108 | # nwo - qualified "owner/repo" name.
109 | #
110 | # Returns the newly created hook URL as String when successful.
111 | # Raises an Error for any other response.
112 | def self.hook_create(nwo)
113 | response = api.create(nwo, @secret, @hook_url)
114 |
115 | if response.code == "201"
116 | Yajl.load(response.body)["url"]
117 | else
118 | Exception.push_http_response(response)
119 | raise Error, "Failed to create hook"
120 | end
121 | end
122 |
123 | # Check existance of a hook.
124 | # http://developer.github.com/v3/repos/hooks/#get-single-hook
125 | #
126 | # url - Hook URL as a String.
127 | def self.hook_exists?(url)
128 | api.get(url).code == "200"
129 | end
130 |
131 | # Delete a post-receive hook for the given repository.
132 | #
133 | # hook_url - The repository's hook_url
134 | #
135 | # Returns true or raises an exception.
136 | def self.hook_delete(url)
137 | response = api.delete(url)
138 |
139 | if response.code == "204"
140 | true
141 | else
142 | Exception.push_http_response(response)
143 | raise Error, "Failed to delete hook"
144 | end
145 | end
146 |
147 | # Default API implementation that goes over the wire (HTTP).
148 | #
149 | # Returns nothing.
150 | def self.api
151 | @api ||= API.new(@api_url, @user, @password)
152 | end
153 |
154 | # Turn on mock mode, meaning no request goes over the wire. Useful in
155 | # testing environments.
156 | #
157 | # Returns nothing.
158 | def self.enable_mock!
159 | @api = Mock.new(@user, @password)
160 | end
161 |
162 | # Make any subsequent response for the given repository look like as if
163 | # it was a private repo.
164 | #
165 | # nwo - qualified "owner/repo" name.
166 | #
167 | # Returns nothing.
168 | def self.repo_make_private(nwo)
169 | api.make_private(nwo)
170 | end
171 |
172 | # Make any subsequent request to the given repository succeed. Only
173 | # available in mock mode.
174 | #
175 | # nwo - qualified "owner/repo" name.
176 | #
177 | # Returns nothing.
178 | def self.repo_make_public(nwo)
179 | api.make_public(nwo)
180 | end
181 |
182 | # Make any subsequent request for the given repository fail with an
183 | # unauthorized response. Only available when mocked.
184 | #
185 | # nwo - qualified "owner/repo" name.
186 | #
187 | # Returns nothing.
188 | def self.repo_make_unauthorized(nwo)
189 | api.make_unauthorized(nwo)
190 | end
191 |
192 | # Set the SHA of the named branch for the given repo. Mock only.
193 | def self.set_branch_head(nwo, branch, sha)
194 | api.set_branch_head(nwo, branch, sha)
195 | end
196 | end
197 | end
198 |
--------------------------------------------------------------------------------
/lib/janky/hubot.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | # Web API taylored for Hubot's needs. Supports setting up and disabling
3 | # repositories, querying the status of branch or a repository and triggering
4 | # builds.
5 | #
6 | # The client side implementation is at
7 | #
8 | class Hubot < Sinatra::Base
9 | register Helpers
10 |
11 | # Setup a new repository.
12 | post "/setup" do
13 | nwo = params["nwo"]
14 | name = params["name"]
15 | tmpl = params["template"]
16 | repo = Repository.setup(nwo, name, tmpl)
17 |
18 | if repo
19 | url = "#{settings.base_url}#{repo.name}"
20 | [201, "Setup #{repo.name} at #{repo.uri} with #{repo.job_config_path.basename} | #{url}"]
21 | else
22 | [400, "Couldn't access #{nwo}. Check the permissions."]
23 | end
24 | end
25 |
26 | # Activate/deactivate auto-build for the given repository.
27 | post "/toggle/:repo_name" do |repo_name|
28 | repo = find_repo(repo_name)
29 | status = repo.toggle_auto_build ? "enabled" : "disabled"
30 |
31 | [200, "#{repo.name} is now #{status}"]
32 | end
33 |
34 | # Build a repository's branch.
35 | post %r{\/([-_\.0-9a-zA-Z]+)\/([-_\.a-zA-z0-9\/]+)} do |repo_name, branch_name|
36 | repo = find_repo(repo_name)
37 | branch = repo.branch_for(branch_name)
38 | room_id = (params["room_id"] rescue nil)
39 | user = params["user"]
40 | build = branch.head_build_for(room_id, user)
41 | build ||= repo.build_sha(branch_name, user, room_id)
42 |
43 | if build
44 | build.run
45 | [201, "Going ham on #{build.repo_name}/#{build.branch_name}"]
46 | else
47 | [404, "Unknown branch #{branch_name.inspect}. Push again"]
48 | end
49 | end
50 |
51 | # Get a list of available rooms.
52 | get "/rooms" do
53 | Yajl.dump(ChatService.room_names)
54 | end
55 |
56 | # Update a repository's notification room.
57 | put "/:repo_name" do |repo_name|
58 | repo = find_repo(repo_name)
59 | room = params["room"]
60 |
61 | if room_id = ChatService.room_id(room)
62 | repo.update_attributes!(:room_id => room_id)
63 | [200, "Room for #{repo.name} updated to #{room}"]
64 | else
65 | [403, "Unknown room: #{room.inspect}"]
66 | end
67 | end
68 |
69 | # Update a repository's context
70 | put %r{\/([-_\.0-9a-zA-Z]+)\/context} do |repo_name|
71 | context = params["context"]
72 | repo = find_repo(repo_name)
73 |
74 | if repo
75 | repo.context = context
76 | repo.save
77 | [200, "Context #{context} set for #{repo_name}"]
78 | else
79 | [404, "Unknown Repository #{repo_name}"]
80 | end
81 | end
82 |
83 | # Get the status of all projects.
84 | get "/" do
85 | content_type "text/plain"
86 | repos = Repository.includes(:branches, :commits, :builds).all.map do |repo|
87 | master = repo.branch_for("master")
88 |
89 | "%-17s %-13s %-10s %40s" % [
90 | repo.name,
91 | master.status,
92 | repo.campfire_room,
93 | repo.uri
94 | ]
95 | end
96 | repos.join("\n")
97 | end
98 |
99 | # Get the lasts builds
100 | get "/builds" do
101 | limit = params["limit"]
102 | building = params["building"]
103 |
104 | builds = Build.unscoped
105 | if building.blank? || building == 'false'
106 | builds = builds.completed
107 | else
108 | builds = builds.building
109 | end
110 | builds = builds.limit(limit) unless limit.blank?
111 |
112 | builds.map do |build|
113 | build_to_hash(build)
114 | end
115 |
116 | builds.to_json
117 | end
118 |
119 | # Get information about how a project is configured
120 | get %r{\/show\/([-_\.0-9a-zA-Z]+)} do |repo_name|
121 | repo = find_repo(repo_name)
122 | res = {
123 | :name => repo.name,
124 | :configured_job_template => repo.job_template,
125 | :used_job_template => repo.job_config_path.basename.to_s,
126 | :repo => repo.uri,
127 | :room_id => repo.room_id,
128 | :enabled => repo.enabled,
129 | :hook_url => repo.hook_url,
130 | :context => repo.context
131 | }
132 | res.to_json
133 | end
134 |
135 | delete %r{\/([-_\.0-9a-zA-Z]+)} do |repo_name|
136 | repo = find_repo(repo_name)
137 | repo.destroy
138 | "Janky project #{repo_name} deleted"
139 | end
140 |
141 | # Delete a repository's context
142 | delete %r{\/([-_\.0-9a-zA-Z]+)\/context} do |repo_name|
143 | repo = find_repo(repo_name)
144 |
145 | if repo
146 | repo.context = nil
147 | repo.save
148 | [200, "Context removed for #{repo_name}"]
149 | else
150 | [404, "Unknown Repository #{repo_name}"]
151 | end
152 | end
153 |
154 | # Get the status of a repository's branch.
155 | get %r{\/([-_\.0-9a-zA-Z]+)\/([-_\+\.a-zA-z0-9\/]+)} do |repo_name, branch_name|
156 | limit = params["limit"]
157 |
158 | repo = find_repo(repo_name)
159 | branch = repo.branch_for(branch_name)
160 | builds = branch.queued_builds.limit(limit).map do |build|
161 | build_to_hash(build)
162 | end
163 |
164 | builds.to_json
165 | end
166 |
167 | # Learn everything you need to know about Janky.
168 | get "/help" do
169 | content_type "text/plain"
170 | <<-EOS
171 | ci build janky
172 | ci build janky/fix-everything
173 | ci setup github/janky [name]
174 | ci setup github/janky name template
175 | ci toggle janky
176 | ci rooms
177 | ci set room janky development
178 | ci set context janky ci/janky
179 | ci unset context janky
180 | ci status
181 | ci status janky
182 | ci status janky/master
183 | ci builds limit [building]
184 | ci show janky
185 | ci delete janky
186 | EOS
187 | end
188 |
189 | get "/boomtown" do
190 | fail "BOOM (janky)"
191 | end
192 |
193 | private
194 |
195 | def build_to_hash(build)
196 | { :sha1 => build.sha1,
197 | :repo => build.repo_name,
198 | :branch => build.branch_name,
199 | :user => build.user,
200 | :green => build.green?,
201 | :building => build.building?,
202 | :queued => build.queued?,
203 | :pending => build.pending?,
204 | :number => build.number,
205 | :status => (build.green? ? "was successful" : "failed"),
206 | :compare => build.compare,
207 | :duration => build.duration,
208 | :web_url => build.web_url }
209 | end
210 | end
211 | end
212 |
--------------------------------------------------------------------------------
/lib/janky/repository.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class Repository < ActiveRecord::Base
3 | has_many :branches, :dependent => :destroy
4 | has_many :commits, :dependent => :destroy
5 | has_many :builds, :through => :branches
6 |
7 | after_commit :delete_hook, :on => :destroy
8 |
9 | replicate_associations :builds, :commits, :branches
10 |
11 | default_scope { order("name") }
12 |
13 | def self.setup(nwo, name = nil, template = nil)
14 | if nwo.nil?
15 | raise ArgumentError, "nwo can't be nil"
16 | end
17 |
18 | if repo = Repository.find_by_name(nwo)
19 | repo.update_attributes!(:job_template => template)
20 | repo.setup
21 | return repo
22 | end
23 |
24 | repo = GitHub.repo_get(nwo)
25 | return if !repo
26 |
27 | uri = repo["private"] ? repo["ssh_url"] : repo["git_url"]
28 | name ||= repo["name"]
29 | uri.gsub!(/\.git$/, "")
30 |
31 | repo =
32 | if repo = Repository.find_by_name(name)
33 | repo.update_attributes!(:uri => uri, :job_template => template)
34 | repo
35 | else
36 | Repository.create!(:name => name, :uri => uri, :job_template => template)
37 | end
38 |
39 | repo.setup
40 | repo
41 | end
42 |
43 | # Find a named repository.
44 | #
45 | # name - The String name of the repository.
46 | #
47 | # Returns a Repository or nil when it doesn't exists.
48 | def self.by_name(name)
49 | find_by_name(name)
50 | end
51 |
52 | # Toggle auto-build feature of this repo. When enabled (default),
53 | # all branches are built automatically.
54 | #
55 | # Returns the new flag status as a Boolean.
56 | def toggle_auto_build
57 | toggle(:enabled)
58 | save!
59 | enabled
60 | end
61 |
62 | # Create or retrieve the named branch.
63 | #
64 | # name - The branch's name as a String.
65 | #
66 | # Returns a Branch record.
67 | def branch_for(name)
68 | branches.find_or_create_by(name: name)
69 | end
70 |
71 | # Create or retrieve the given commit.
72 | #
73 | # commit - The Hash representation of the Commit.
74 | #
75 | # Returns a Commit record.
76 | def commit_for(commit)
77 | commits.find_by_sha1(commit[:sha1]) ||
78 | commits.create!(commit)
79 | end
80 |
81 | def commit_for_sha(sha1)
82 | commit_data = GitHub.commit(nwo, sha1)
83 | commit_message = commit_data["commit"]["message"]
84 | commit_url = github_url("commit/#{sha1}")
85 | author_data = commit_data["commit"]["author"]
86 | commit_author =
87 | if email = author_data["email"]
88 | "#{author_data["name"]} <#{email}>"
89 | else
90 | author_data["name"]
91 | end
92 |
93 | commit = commit_for({
94 | :repository => self,
95 | :sha1 => sha1,
96 | :author => commit_author,
97 | :message => commit_message,
98 | :url => commit_url,
99 | })
100 | end
101 |
102 | # Create a Janky::Build object given a sha
103 | #
104 | # sha1 - a string of the target sha to build
105 | # user - The login of the GitHub user who pushed.
106 | # room_id - optional Fixnum Campfire room ID. Defaults to the room set on
107 | # compare - optional String GitHub Compare View URL. Defaults to the
108 | #
109 | # Returns the newly created Janky::Build
110 | def build_sha(sha1, user, room_id = nil, compare = nil)
111 | return nil unless sha1 =~ /^[0-9a-fA-F]{7,40}$/
112 | commit = commit_for_sha(sha1)
113 | commit.build!(user, room_id, compare)
114 | end
115 |
116 | # Jenkins host executing this repo's builds.
117 | #
118 | # Returns a Builder::Client.
119 | def builder
120 | Builder.pick_for(self)
121 | end
122 |
123 | # GitHub user owning this repo.
124 | #
125 | # Returns the user name as a String.
126 | def github_owner
127 | uri[/.*[\/:]([a-zA-Z0-9\-_]+)\//] && $1
128 | end
129 |
130 | # Name of this repository on GitHub.
131 | #
132 | # Returns the name as a String.
133 | def github_name
134 | uri[/.*[\/:]([a-zA-Z0-9\-_]+)\/([a-zA-Z0-9\-_\.]+)/] && $2
135 | end
136 |
137 | # Fully qualified GitHub name for this repository.
138 | #
139 | # Returns the name as a String. Example: github/janky.
140 | def nwo
141 | "#{github_owner}/#{github_name}"
142 | end
143 |
144 | # Append the given path to the GitHub URL of this repository.
145 | #
146 | # path - String path. No slash necessary at the front.
147 | #
148 | # Examples
149 | #
150 | # github_url("issues")
151 | # => "https://github.com/github/janky/issues"
152 | #
153 | # Returns the URL as a String.
154 | def github_url(path)
155 | "#{GitHub.github_url}/#{nwo}/#{path}"
156 | end
157 |
158 | # Name of the Campfire room receiving build notifications.
159 | #
160 | # Returns the name as a String.
161 | def campfire_room
162 | ChatService.room_name(room_id)
163 | end
164 |
165 | # Ditto but returns the String room id. Defaults to the one set
166 | # in Campfire.setup.
167 | def room_id
168 | read_attribute(:room_id) || ChatService.default_room_id
169 | end
170 |
171 | # Setups GitHub and Jenkins for building this repository.
172 | #
173 | # Returns nothing.
174 | def setup
175 | setup_job
176 | setup_hook
177 | end
178 |
179 | # Create a GitHub hook for this Repository and store its URL if needed.
180 | #
181 | # Returns nothing.
182 | def setup_hook
183 | delete_hook
184 |
185 | url = GitHub.hook_create("#{github_owner}/#{github_name}")
186 | update_attributes!(:hook_url => url)
187 | end
188 |
189 | def delete_hook
190 | if self.hook_url? && GitHub.hook_exists?(self.hook_url)
191 | GitHub.hook_delete(self.hook_url)
192 | end
193 | end
194 |
195 | # Creates a job on the Jenkins server for this repository configuration
196 | # unless one already exists. Can safely be run multiple times.
197 | #
198 | # Returns nothing.
199 | def setup_job
200 | builder.setup(job_name, uri, job_config_path)
201 | end
202 |
203 | # The path of the Jenkins configuration template. Try
204 | # ".xml.erb" first, ".xml.erb" second, and then
205 | # fallback to "default.xml.erb" under the root config directory.
206 | #
207 | # Returns the template path as a Pathname.
208 | def job_config_path
209 | user_override = Janky.jobs_config_dir.join("#{job_template.downcase}.xml.erb") if job_template
210 | custom = Janky.jobs_config_dir.join("#{name.downcase}.xml.erb")
211 | default = Janky.jobs_config_dir.join("default.xml.erb")
212 |
213 | if user_override && user_override.readable?
214 | user_override
215 | elsif custom.readable?
216 | custom
217 | elsif default.readable?
218 | default
219 | else
220 | raise Error, "no config.xml.erb template for repo #{id.inspect}"
221 | end
222 | end
223 |
224 | # Construct the URL pointing to this Repository's Jenkins job.
225 | #
226 | # Returns the String URL.
227 | def job_url
228 | builder.url + "job/#{job_name}"
229 | end
230 |
231 | # Calculate the name of the Jenkins job.
232 | #
233 | # Returns a String hash of this Repository name and uri.
234 | def job_name
235 | md5 = Digest::MD5.new
236 | md5 << name
237 | md5 << uri
238 | md5 << job_config_path.read
239 | md5 << builder.callback_url.to_s
240 | "#{name}-#{md5.hexdigest[0,12]}"
241 | end
242 | end
243 | end
244 |
--------------------------------------------------------------------------------
/lib/janky/build.rb:
--------------------------------------------------------------------------------
1 | module Janky
2 | class Build < ActiveRecord::Base
3 | belongs_to :branch
4 | belongs_to :commit
5 |
6 | default_scope do
7 | columns = (column_names - ["output"]).map do |column_name|
8 | arel_table[column_name]
9 | end
10 |
11 | select(columns)
12 | end
13 |
14 | scope :building, lambda {
15 | where("started_at IS NOT NULL AND completed_at IS NULL")
16 | }
17 |
18 | # Transition the Build to the started state.
19 | #
20 | # id - the Fixnum ID used to find the build.
21 | # url - the full String URL of the build.
22 | #
23 | # Returns nothing or raises an Error for inexistant builds.
24 | def self.start(id, url)
25 | if build = find_by_id(id)
26 | build.start(url, Time.now)
27 | else
28 | raise Error, "Unknown build: #{id.inspect}"
29 | end
30 | end
31 |
32 | # Transition the Build to the completed state.
33 | #
34 | # id - the Fixnum ID used to find the build.
35 | # green - Boolean indicating build success.
36 | #
37 | # Returns nothing or raises an Error for inexistant builds.
38 | def self.complete(id, green)
39 | if build = find_by_id(id)
40 | build.complete(green, Time.now)
41 | else
42 | raise Error, "Unknown build: #{id.inspect}"
43 | end
44 | end
45 |
46 | # Find all builds that have been queued in Jenkins, most recent first.
47 | #
48 | # Returns an Array of Build objects.
49 | def self.queued
50 | where("queued_at IS NOT NULL").order("queued_at DESC, id DESC")
51 | end
52 |
53 | # Find all started builds, most recent first.
54 | #
55 | # Returns an Array of Builds.
56 | def self.started
57 | where("started_at IS NOT NULL").order("started_at DESC, id DESC")
58 | end
59 |
60 | # Find all completed builds, most recent first.
61 | #
62 | # Returns an Array of Builds.
63 | def self.completed
64 | started.
65 | where("completed_at IS NOT NULL")
66 | end
67 |
68 | # Find all green builds, most recent first.
69 | #
70 | # Returns an Array of Builds.
71 | def self.green
72 | completed.where(:green => true)
73 | end
74 |
75 | # Has this build been queued in Jenkins?
76 | #
77 | # Returns true when the build is complete or currently being built,
78 | # false otherwise.
79 | def queued?
80 | ! queued_at.nil?
81 | end
82 |
83 | # Is this build currently sitting in the queue waiting to be built?
84 | #
85 | # Returns true if the build is queued and not started, false otherwise.
86 | def pending?
87 | queued? && !started?
88 | end
89 |
90 | # Is this build currently being built?
91 | #
92 | # Returns a Boolean.
93 | def building?
94 | started? && !completed?
95 | end
96 |
97 | # Is this build red?
98 | #
99 | # Returns a Boolean, nothing when the build hasn't completed yet.
100 | def red?
101 | completed? && !green?
102 | end
103 |
104 | # Was this build ever started?
105 | #
106 | # Returns a Boolean.
107 | def started?
108 | ! started_at.nil?
109 | end
110 |
111 | # Did this build complete?
112 | #
113 | # Returns a Boolean.
114 | def completed?
115 | ! completed_at.nil?
116 | end
117 |
118 | # Trigger a Jenkins build using the appropriate builder.
119 | #
120 | # Returns nothing.
121 | def run
122 | builder.run(self)
123 | update_attributes!(:queued_at => Time.now)
124 | end
125 |
126 | # See Repository#builder.
127 | def builder
128 | branch.repository.builder
129 | end
130 |
131 | # Run a copy of itself. Typically used to force a build in case of
132 | # temporary test failure or when auto-build is disabled.
133 | #
134 | # new_room_id - optional Campfire room String ID. Defaults to the room of the
135 | # build being re-run.
136 | #
137 | # Returns the build copy.
138 | def rerun(new_room_id = nil)
139 | build = branch.build_for(commit, new_room_id)
140 | build.run
141 | build
142 | end
143 |
144 | # Cached or remote build output.
145 | #
146 | # Returns the String output.
147 | def output
148 | if completed?
149 | read_attribute(:output)
150 | elsif started?
151 | output_remote
152 | else
153 | ""
154 | end
155 | end
156 |
157 | # Retrieve the build output from the Jenkins server.
158 | #
159 | # Returns the String output.
160 | def output_remote
161 | if started?
162 | builder.output(self)
163 | end
164 | end
165 |
166 | # Mark the build as started.
167 | #
168 | # url - the full String URL of the build on the Jenkins server.
169 | # now - the Time at which the build started.
170 | #
171 | # Returns nothing or raise an Error for weird transitions.
172 | def start(url, now)
173 | if started?
174 | raise Error, "Build #{id} already started"
175 | elsif completed?
176 | raise Error, "Build #{id} already completed"
177 | else
178 | update_attributes!(:url => url, :started_at => now)
179 | Notifier.started(self)
180 | end
181 | end
182 |
183 | # Mark the build as complete, store the build output and notify Campfire.
184 | #
185 | # green - Boolean indicating build success.
186 | # now - the Time at which the build completed.
187 | #
188 | # Returns nothing or raise an Error for weird transitions.
189 | def complete(green, now)
190 | if ! started?
191 | raise Error, "Build #{id} not started"
192 | elsif completed?
193 | raise Error, "Build #{id} already completed"
194 | else
195 | update_attributes!(
196 | :green => green,
197 | :completed_at => now,
198 | :output => output_remote
199 | )
200 | Notifier.completed(self)
201 | end
202 | end
203 |
204 | # The time it took to peform this build in seconds.
205 | #
206 | # Returns an Integer seconds.
207 | def duration
208 | if completed?
209 | Integer(completed_at - started_at)
210 | end
211 | end
212 |
213 | # The name of the Campfire room where notifications are sent.
214 | #
215 | # Returns the String room name.
216 | def room_name
217 | if room_id && !room_id.empty?
218 | ChatService.room_name(room_id)
219 | end
220 | end
221 |
222 | class << self
223 | # The full URL of the web app as a String, including the protocol.
224 | attr_accessor :base_url
225 |
226 | # The full URL to the Jenkins build page, as a String.
227 | attr_reader :url
228 | end
229 |
230 | # URL of this build's web page, served by Janky::App.
231 | #
232 | # Returns the URL as a String.
233 | def web_url
234 | return if new_record?
235 | self.class.base_url + "#{id}/output"
236 | end
237 |
238 | # URL of the web page for this build's branch, served by Janky::App.
239 | #
240 | # Returns the URL as a String.
241 | def branch_url
242 | return if new_record?
243 | self.class.base_url + "#{repo_name}/#{branch_name}"
244 | end
245 |
246 | def repo_id
247 | repository.id
248 | end
249 |
250 | def repo_job_name
251 | repository.job_name
252 | end
253 |
254 | def repo_name
255 | repository.name
256 | end
257 |
258 | def repo_nwo
259 | repository.nwo
260 | end
261 |
262 | def repository
263 | branch.repository
264 | end
265 |
266 | def repo
267 | branch.repository
268 | end
269 |
270 | def sha1
271 | commit.sha1
272 | end
273 |
274 | def short_sha1
275 | sha1[0,7]
276 | end
277 |
278 | def commit_url
279 | commit.url
280 | end
281 |
282 | def commit_message
283 | commit.message
284 | end
285 |
286 | def commit_author
287 | commit.author
288 | end
289 |
290 | def number
291 | id.to_s
292 | end
293 |
294 | def branch_name
295 | branch.name
296 | end
297 | end
298 | end
299 |
--------------------------------------------------------------------------------
/lib/janky.rb:
--------------------------------------------------------------------------------
1 | if RUBY_VERSION < "1.9.3"
2 | warn "Support for Ruby versions lesser than 1.9.3 is deprecated and will be " \
3 | "removed in Janky 1.0."
4 | end
5 |
6 | require "net/http"
7 | require "digest/md5"
8 |
9 | require "active_record"
10 | require "replicate"
11 | require "sinatra/base"
12 | require "mustache/sinatra"
13 | require "yajl"
14 | require "yajl/json_gem"
15 | require "tilt"
16 | require "broach"
17 | require "sinatra/auth/github"
18 |
19 | require "janky/repository"
20 | require "janky/branch"
21 | require "janky/commit"
22 | require "janky/build"
23 | require "janky/build_request"
24 | require "janky/github"
25 | require "janky/github/api"
26 | require "janky/github/mock"
27 | require "janky/github/payload"
28 | require "janky/github/commit"
29 | require "janky/github/payload_parser"
30 | require "janky/github/receiver"
31 | require "janky/job_creator"
32 | require "janky/helpers"
33 | require "janky/hubot"
34 | require "janky/builder"
35 | require "janky/builder/client"
36 | require "janky/builder/runner"
37 | require "janky/builder/http"
38 | require "janky/builder/mock"
39 | require "janky/builder/payload"
40 | require "janky/builder/receiver"
41 | require "janky/chat_service"
42 | require "janky/chat_service/campfire"
43 | require "janky/chat_service/mock"
44 | require "janky/exception"
45 | require "janky/notifier"
46 | require "janky/notifier/chat_service"
47 | require "janky/notifier/failure_service"
48 | require "janky/notifier/mock"
49 | require "janky/notifier/multi"
50 | require "janky/notifier/github_status"
51 | require "janky/app"
52 | require "janky/views/layout"
53 | require "janky/views/index"
54 | require "janky/views/console"
55 |
56 | # TODO Remove after upgrading to activerecord 4.x
57 | require "active_record/connection_adapters/mysql2_adapter"
58 | class ActiveRecord::ConnectionAdapters::Mysql2Adapter
59 | NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
60 | end
61 |
62 | # This is Janky, a continuous integration server. Checkout the 'app'
63 | # method on this module for an overview of the different components
64 | # involved.
65 | module Janky
66 | # The base exception class raised when errors are encountered.
67 | class Error < StandardError; end
68 |
69 | # Setup the application, including the database and Jenkins connections.
70 | #
71 | # settings - Hash of app settings. Typically ENV but any object that responds
72 | # to #[], #[]= and #each is valid. See required_settings for
73 | # required keys. The RACK_ENV key is always required.
74 | #
75 | # Raises an Error when required settings are missing.
76 | # Returns nothing.
77 | def self.setup(settings)
78 | env = settings["RACK_ENV"]
79 | if env.nil? || env.empty?
80 | raise Error, "RACK_ENV is required"
81 | end
82 |
83 | required_settings.each do |setting|
84 | next if !settings[setting].nil? && !settings[setting].empty?
85 |
86 | if env == "production"
87 | raise Error, "#{setting} setting is required"
88 | end
89 | end
90 |
91 | if env != "production"
92 | settings["DATABASE_URL"] ||= "mysql2://root@localhost/janky_#{env}"
93 | settings["JANKY_BASE_URL"] ||= "http://localhost:9393/"
94 | settings["JANKY_BUILDER_DEFAULT"] ||= "http://localhost:8080/"
95 | settings["JANKY_CONFIG_DIR"] ||= File.dirname(__FILE__)
96 | settings["JANKY_CHAT"] ||= "campfire"
97 | settings["JANKY_CHAT_CAMPFIRE_ACCOUNT"] ||= "account"
98 | settings["JANKY_CHAT_CAMPFIRE_TOKEN"] ||= "token"
99 | end
100 |
101 | database = URI(settings["DATABASE_URL"])
102 | adapter = database.scheme == "postgres" ? "postgresql" : database.scheme
103 | encoding = database.scheme == "postgres" ? "unicode" : "utf8"
104 | base_url = URI(settings["JANKY_BASE_URL"]).to_s
105 | Build.base_url = base_url
106 |
107 | connection = {
108 | :adapter => adapter,
109 | :encoding => encoding,
110 | :pool => 5,
111 | :database => database.path[1..-1],
112 | :username => database.user,
113 | :password => database.password,
114 | :host => database.host,
115 | :port => database.port,
116 | :reconnect => true,
117 | }
118 | if socket = settings["JANKY_DATABASE_SOCKET"]
119 | connection[:socket] = socket
120 | end
121 | ActiveRecord::Base.establish_connection(connection)
122 |
123 | self.jobs_config_dir = config_dir = Pathname(settings["JANKY_CONFIG_DIR"])
124 | if !config_dir.directory?
125 | raise Error, "#{config_dir} is not a directory"
126 | end
127 |
128 | # Setup the callback URL of this Janky host.
129 | Janky::Builder.setup(base_url + "_builder")
130 |
131 | # Setup the default Jenkins build host
132 | if settings["JANKY_BUILDER_DEFAULT"][-1] != ?/
133 | raise Error, "JANKY_BUILDER_DEFAULT must have a trailing slash"
134 | end
135 | Janky::Builder[:default] = settings["JANKY_BUILDER_DEFAULT"]
136 |
137 | if settings.key?("JANKY_GITHUB_API_URL")
138 | api_url = settings["JANKY_GITHUB_API_URL"]
139 | git_host = URI(api_url).host
140 | else
141 | api_url = "https://api.github.com/"
142 | git_host = "github.com"
143 | end
144 | if api_url[-1] != ?/
145 | raise Error, "JANKY_GITHUB_API_URL must have a trailing slash"
146 | end
147 | hook_url = base_url + "_github"
148 | Janky::GitHub.setup(
149 | settings["JANKY_GITHUB_USER"],
150 | settings["JANKY_GITHUB_PASSWORD"],
151 | settings["JANKY_GITHUB_HOOK_SECRET"],
152 | hook_url,
153 | api_url,
154 | git_host
155 | )
156 |
157 | if settings.key?("JANKY_SESSION_SECRET")
158 | Janky::App.register Sinatra::Auth::Github
159 | Janky::App.set({
160 | :sessions => true,
161 | :session_secret => settings["JANKY_SESSION_SECRET"],
162 | :github_team_id => settings["JANKY_AUTH_TEAM_ID"],
163 | :github_organization => settings["JANKY_AUTH_ORGANIZATION"],
164 | :github_options => {
165 | :secret => settings["JANKY_AUTH_CLIENT_SECRET"],
166 | :client_id => settings["JANKY_AUTH_CLIENT_ID"],
167 | :scopes => "repo",
168 | },
169 | })
170 | end
171 |
172 | Janky::Hubot.set(
173 | :base_url => settings["JANKY_BASE_URL"],
174 | :username => settings["JANKY_HUBOT_USER"],
175 | :password => settings["JANKY_HUBOT_PASSWORD"]
176 | )
177 |
178 | Janky::Exception.setup(Janky::Exception::Logger.new($stderr))
179 |
180 | if campfire_account = settings["JANKY_CAMPFIRE_ACCOUNT"]
181 | warn "JANKY_CAMPFIRE_ACCOUNT is deprecated. Please use " \
182 | "JANKY_CHAT_CAMPFIRE_ACCOUNT instead."
183 | settings["JANKY_CHAT_CAMPFIRE_ACCOUNT"] ||=
184 | settings["JANKY_CAMPFIRE_ACCOUNT"]
185 | end
186 |
187 | if campfire_token = settings["JANKY_CAMPFIRE_TOKEN"]
188 | warn "JANKY_CAMPFIRE_TOKEN is deprecated. Please use " \
189 | "JANKY_CHAT_CAMPFIRE_TOKEN instead."
190 | settings["JANKY_CHAT_CAMPFIRE_TOKEN"] ||=
191 | settings["JANKY_CAMPFIRE_TOKEN"]
192 | end
193 |
194 | chat_name = settings["JANKY_CHAT"] || "campfire"
195 | chat_settings = {}
196 | settings.each do |key, value|
197 | if key =~ /^JANKY_CHAT_#{chat_name.upcase}_/
198 | chat_settings[key] = value
199 | end
200 | end
201 | chat_room = settings["JANKY_CHAT_DEFAULT_ROOM"] ||
202 | settings["JANKY_CAMPFIRE_DEFAULT_ROOM"]
203 | if settings["JANKY_CAMPFIRE_DEFAULT_ROOM"]
204 | warn "JANKY_CAMPFIRE_DEFAULT_ROOM is deprecated. Please use " \
205 | "JANKY_CHAT_DEFAULT_ROOM instead."
206 | end
207 | ChatService.setup(chat_name, chat_settings, chat_room)
208 |
209 | if token = settings["JANKY_GITHUB_STATUS_TOKEN"]
210 | context = settings["JANKY_GITHUB_STATUS_CONTEXT"]
211 | Notifier.setup([
212 | Notifier::GithubStatus.new(token, api_url, context),
213 | Notifier::ChatService,
214 | Notifier::FailureService
215 | ])
216 | else
217 | Notifier.setup(Notifier::ChatService)
218 | end
219 | end
220 |
221 | # List of settings required in production.
222 | #
223 | # Returns an Array of Strings.
224 | def self.required_settings
225 | %w[RACK_ENV DATABASE_URL
226 | JANKY_BASE_URL
227 | JANKY_BUILDER_DEFAULT
228 | JANKY_CONFIG_DIR
229 | JANKY_GITHUB_USER JANKY_GITHUB_PASSWORD JANKY_GITHUB_HOOK_SECRET
230 | JANKY_HUBOT_USER JANKY_HUBOT_PASSWORD]
231 | end
232 |
233 | class << self
234 | # Directory where Jenkins job configuration templates are located.
235 | #
236 | # Returns the directory as a Pathname.
237 | attr_accessor :jobs_config_dir
238 | end
239 |
240 | # Mock out all network-dependant components. Must be called after setup.
241 | # Typically used in test environments.
242 | #
243 | # Returns nothing.
244 | def self.enable_mock!
245 | Janky::Builder.enable_mock!
246 | Janky::GitHub.enable_mock!
247 | Janky::Notifier.enable_mock!
248 | Janky::ChatService.enable_mock!
249 | Janky::App.disable :github_team_id
250 | end
251 |
252 | # Reset the state of the mocks.
253 | #
254 | # Returns nothing.
255 | def self.reset!
256 | Janky::Notifier.reset!
257 | Janky::Builder.reset!
258 | end
259 |
260 | # The Janky Rack application, assembled from four apps. Exceptions raised
261 | # during the request cycle are caught by the Exception middleware which
262 | # typically reports them to an external service before re-raising the
263 | # exception.
264 | #
265 | # Returns a memoized Rack application.
266 | def self.app
267 | @app ||= Rack::Builder.app {
268 | # GitHub Post-Receive requests.
269 | map "/_github" do
270 | run Janky::GitHub.receiver
271 | end
272 |
273 | # Jenkins callback requests.
274 | map "/_builder" do
275 | run Janky::Builder.receiver
276 | end
277 |
278 | # Hubot API, protected by Basic Auth.
279 | map "/_hubot" do
280 | use Rack::Auth::Basic do |username, password|
281 | username == Janky::Hubot.username &&
282 | password == Janky::Hubot.password
283 | end
284 |
285 | run Janky::Hubot
286 | end
287 |
288 | # Web dashboard
289 | map "/" do
290 | run Janky::App
291 | end
292 | }
293 | end
294 |
295 | # Register a Chat service implementation.
296 | #
297 | # name - Service name as a String, e.g. "irc".
298 | # service - Constant for the implementation.
299 | #
300 | # Returns nothing.
301 | def self.register_chat_service(name, service)
302 | Janky::ChatService.adapters[name] = service
303 | end
304 |
305 | register_chat_service "campfire", ChatService::Campfire
306 | end
307 |
--------------------------------------------------------------------------------
/test/janky_test.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path("../test_helper", __FILE__)
2 |
3 | class JankyTest < Test::Unit::TestCase
4 | def setup
5 | Janky.setup(environment)
6 | Janky.enable_mock!
7 | Janky.reset!
8 |
9 | DatabaseCleaner.clean_with(:truncation)
10 |
11 | Janky::ChatService.rooms = {1 => "enterprise", "2" => "builds"}
12 | Janky::ChatService.default_room_name = "builds"
13 |
14 | hubot_setup("github/github")
15 | end
16 |
17 | test "green build" do
18 | Janky::Builder.green!
19 | gh_post_receive("github")
20 | Janky::Builder.start!
21 | Janky::Builder.complete!
22 |
23 | assert Janky::Notifier.success?("github", "master")
24 | end
25 |
26 | test "fail build" do
27 | Janky::Builder.red!
28 | gh_post_receive("github")
29 | Janky::Builder.start!
30 | Janky::Builder.complete!
31 |
32 | assert Janky::Notifier.failure?("github", "master")
33 | end
34 |
35 | test "pending build" do
36 | Janky::Builder.green!
37 | gh_post_receive("github")
38 |
39 | assert Janky::Notifier.empty?
40 | Janky::Builder.start!
41 | Janky::Builder.complete!
42 | assert Janky::Notifier.success?("github", "master")
43 | end
44 |
45 | test "builds multiple repo with the same uri" do
46 | Janky::Builder.green!
47 | hubot_setup("github/github", "fi")
48 | gh_post_receive("github")
49 | Janky::Builder.start!
50 | Janky::Builder.complete!
51 |
52 | assert Janky::Notifier.success?("github", "master")
53 | assert Janky::Notifier.success?("fi", "master")
54 | end
55 |
56 | test "notifies room that triggered the build" do
57 | Janky::Builder.green!
58 | gh_post_receive("github")
59 | Janky::Builder.start!
60 | Janky::Builder.complete!
61 |
62 | assert Janky::Notifier.success?("github", "master", "builds")
63 |
64 | hubot_build("github", "master", "enterprise")
65 | Janky::Builder.start!
66 | Janky::Builder.complete!
67 |
68 | assert Janky::Notifier.success?("github", "master", "enterprise")
69 | end
70 |
71 | test "dup commit same branch" do
72 | Janky::Builder.green!
73 | gh_post_receive("github", "master", "sha1")
74 | Janky::Builder.start!
75 | Janky::Builder.complete!
76 |
77 | assert Janky::Notifier.notifications.shift
78 |
79 | gh_post_receive("github", "master", "sha1")
80 | Janky::Builder.start!
81 | Janky::Builder.complete!
82 |
83 | assert Janky::Notifier.notifications.empty?
84 | end
85 |
86 | test "dup commit different branch" do
87 | Janky::Builder.green!
88 | gh_post_receive("github", "master", "sha1")
89 | Janky::Builder.start!
90 | Janky::Builder.complete!
91 |
92 | assert Janky::Notifier.notifications.shift
93 |
94 | gh_post_receive("github", "issues-dashboard", "sha1")
95 | Janky::Builder.start!
96 | Janky::Builder.complete!
97 |
98 | assert Janky::Notifier.notifications.empty?
99 | end
100 |
101 | test "dup commit currently building" do
102 | Janky::Builder.green!
103 | gh_post_receive("github", "master", "sha1")
104 | Janky::Builder.start!
105 |
106 | gh_post_receive("github", "issues-dashboard", "sha1")
107 |
108 | Janky::Builder.complete!
109 |
110 | assert_equal 1, Janky::Notifier.notifications.size
111 | assert Janky::Notifier.success?("github", "master")
112 | end
113 |
114 | test "dup commit currently red" do
115 | Janky::Builder.red!
116 | gh_post_receive("github", "master", "sha1")
117 | Janky::Builder.start!
118 | Janky::Builder.complete!
119 |
120 | assert Janky::Notifier.notifications.shift
121 |
122 | gh_post_receive("github", "master", "sha1")
123 |
124 | assert Janky::Notifier.notifications.empty?
125 | end
126 |
127 | test "dup commit disabled repo" do
128 | hubot_setup("github/github", "fi")
129 | hubot_toggle("fi")
130 | gh_post_receive("github", "master")
131 | Janky::Builder.start!
132 | Janky::Builder.complete!
133 | Janky::Notifier.reset!
134 |
135 | hubot_build("fi", "master")
136 | Janky::Builder.start!
137 | Janky::Builder.complete!
138 | assert Janky::Notifier.success?("fi", "master")
139 | end
140 |
141 | test "web dashboard" do
142 | assert get("/").ok?
143 | assert get("/janky").not_found?
144 |
145 | gh_post_receive("github")
146 | assert get("/").ok?
147 | assert get("/github").ok?
148 |
149 | Janky::Builder.start!
150 | assert get("/").ok?
151 |
152 | Janky::Builder.complete!
153 | assert get("/").ok?
154 | assert get("/github").ok?
155 |
156 | assert get("/github/master").ok?
157 | assert get("/github/strato").ok?
158 |
159 | assert get("#{Janky::Build.last.id}/output").ok?
160 | end
161 |
162 | test "hubot setup" do
163 | Janky::GitHub.repo_make_private("github/github")
164 | assert hubot_setup("github/github").body.
165 | include?("git@github.com:github/github")
166 |
167 | Janky::GitHub.repo_make_public("github/github")
168 | assert hubot_setup("github/github").body.
169 | include?("git://github.com/github/github")
170 |
171 | assert_equal 1, hubot_status.body.split("\n").size
172 |
173 | hubot_setup("github/janky")
174 | assert_equal 2, hubot_status.body.split("\n").size
175 |
176 | Janky::GitHub.repo_make_unauthorized("github/enterprise")
177 | assert hubot_setup("github/enterprise").body.
178 | include?("Couldn't access github/enterprise")
179 |
180 | assert_equal 201, hubot_setup("janky").status
181 | end
182 |
183 | test "hubot toggle" do
184 | hubot_toggle("github")
185 | gh_post_receive("github", "master", "deadbeef")
186 | Janky::Builder.start!
187 | Janky::Builder.complete!
188 |
189 | assert Janky::Notifier.empty?
190 |
191 | hubot_toggle("github")
192 | gh_post_receive("github", "master", "cream")
193 | Janky::Builder.start!
194 | Janky::Builder.complete!
195 |
196 | assert Janky::Notifier.success?("github", "master")
197 | end
198 |
199 | test "hubot status" do
200 | gh_post_receive("github")
201 | Janky::Builder.start!
202 | Janky::Builder.complete!
203 |
204 | status = hubot_status.body
205 | assert status.include?("github")
206 | assert status.include?("green")
207 | assert status.include?("builds")
208 |
209 | hubot_build("github", "master")
210 | assert hubot_status.body.include?("green")
211 |
212 | Janky::Builder.start!
213 | assert hubot_status.body.include?("building")
214 |
215 | hubot_setup("github/janky")
216 | assert hubot_status.body.include?("no build")
217 |
218 | hubot_setup("github/team")
219 | gh_post_receive("team")
220 | assert hubot_status.ok?
221 | end
222 |
223 | test "build user" do
224 | gh_post_receive("github", "master", "HEAD", "the dude")
225 | Janky::Builder.start!
226 | Janky::Builder.complete!
227 |
228 | response = hubot_status("github", "master")
229 | data = Yajl.load(response.body)
230 | assert_equal 1, data.size
231 | build = data[0]
232 | assert_equal "the dude", build["user"]
233 |
234 | hubot_build("github", "master", nil, "the boyscout")
235 | Janky::Builder.start!
236 | Janky::Builder.complete!
237 |
238 | response = hubot_status("github", "master")
239 | data = Yajl.load(response.body)
240 | assert_equal 2, data.size
241 | build = data[0]
242 | assert_equal "the boyscout", build["user"]
243 | end
244 |
245 | test "hubot status repo" do
246 | gh_post_receive("github")
247 | payload = Yajl.load(hubot_status("github", "master").body)
248 | assert_equal 1, payload.size
249 | build = payload[0]
250 | assert build["queued"]
251 | assert build["pending"]
252 | assert !build["building"]
253 |
254 | Janky::Builder.start!
255 | Janky::Builder.complete!
256 | hubot_build("github", "master")
257 | Janky::Builder.start!
258 | Janky::Builder.complete!
259 |
260 | payload = Yajl.load(hubot_status("github", "master").body)
261 |
262 | assert_equal 2, payload.size
263 | end
264 |
265 | test "hubot last 1 builds" do
266 | 3.times do
267 | gh_post_receive("github", "master")
268 | Janky::Builder.start!
269 | Janky::Builder.complete!
270 | end
271 |
272 | assert_equal 1, Yajl.load(hubot_last(limit: 1).body).size
273 | end
274 |
275 | test "hubot lasts completed" do
276 | gh_post_receive("github", "master")
277 | Janky::Builder.start!
278 | Janky::Builder.complete!
279 |
280 | assert_equal 1, Yajl.load(hubot_last.body).size
281 | end
282 |
283 | test "hubot lasts building" do
284 | gh_post_receive("github", "master")
285 | Janky::Builder.start!
286 | assert_equal 1, Yajl.load(hubot_last(building: true).body).size
287 | end
288 |
289 | test "hubot build" do
290 | gh_post_receive("github", "master")
291 | Janky::Builder.start!
292 | Janky::Builder.complete!
293 |
294 | assert hubot_build("github", "rails3").not_found?
295 | end
296 |
297 | test "hubot build sha" do
298 | gh_post_receive("github", "master", 'deadbeef')
299 | gh_post_receive("github", "master", 'cafebabe')
300 | Janky::Builder.start!
301 | Janky::Builder.complete!
302 |
303 | assert_equal "cafebabe", hubot_latest_build_sha("github", "master")
304 |
305 | hubot_build("github", "deadbeef")
306 | Janky::Builder.start!
307 | Janky::Builder.complete!
308 | assert_equal "deadbeef", hubot_latest_build_sha("github", "master")
309 | end
310 |
311 | test "getting latest commit" do
312 | gh_post_receive("github", "master")
313 | Janky::Builder.start!
314 | Janky::Builder.complete!
315 |
316 | assert_not_equal "deadbeef", hubot_latest_build_sha("github", "master")
317 |
318 | Janky::GitHub.set_branch_head("github/github", "master", "deadbeef")
319 | hubot_build("github", "master")
320 | Janky::Builder.start!
321 | Janky::Builder.complete!
322 |
323 | assert_equal "deadbeef", hubot_latest_build_sha("github", "master")
324 | assert_equal "deadbeef", Janky::Build.last.sha1
325 | assert_equal "Test Author ", Janky::Build.last.commit_author
326 | assert_equal "Test Message", Janky::Build.last.commit_message
327 | assert_equal "https://github.com/github/github/commit/deadbeef", Janky::Build.last.commit_url
328 | end
329 |
330 | test "hubot rooms" do
331 | response = hubot_request("GET", "/_hubot/rooms")
332 | rooms = Yajl.load(response.body)
333 | assert_equal ["builds", "enterprise"], rooms
334 | end
335 |
336 | test "hubot set room" do
337 | gh_post_receive("github", "master")
338 | Janky::Builder.start!
339 | Janky::Builder.complete!
340 | assert Janky::Notifier.success?("github", "master", "builds")
341 |
342 | Janky::Notifier.reset!
343 |
344 | hubot_update_room("github", "enterprise").ok?
345 | hubot_build("github", "master")
346 | Janky::Builder.start!
347 | Janky::Builder.complete!
348 |
349 | assert Janky::Notifier.success?("github", "master", "enterprise")
350 | end
351 |
352 | test "hubot 404s" do
353 | assert hubot_status("janky", "master").not_found?
354 | assert hubot_build("janky", "master").not_found?
355 | assert hubot_build("github", "master").not_found?
356 | end
357 |
358 | test "build janky url" do
359 | gh_post_receive("github")
360 | Janky::Builder.start!
361 | Janky::Builder.complete!
362 |
363 | assert_equal "http://localhost:9393/1/output", Janky::Build.last.web_url
364 |
365 | build_page = Janky::Build.last.repo_job_name + "/" + Janky::Build.last.number + "/"
366 | assert_equal "http://localhost:8080/job/" + build_page, Janky::Build.last.url
367 | end
368 | end
369 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Janky
2 | =====
3 |
4 | [](https://badge.fury.io/rb/janky)
5 |
6 | This is Janky, a continuous integration server built on top of
7 | [Jenkins][], controlled by [Hubot][], and designed for [GitHub][].
8 |
9 | * **Built on top of Jenkins.** The power, vast amount of plugins and large
10 | community of the popular CI server all wrapped up in a great experience.
11 |
12 | * **Controlled by Hubot.** Day to day operations are exposed as simple
13 | Hubot commands that the whole team can use.
14 |
15 | * **Designed for GitHub.** Janky creates the appropriate [web hooks][w] for
16 | you and the web app restricts access to members of your GitHub organization.
17 |
18 | [GitHub]: https://github.com
19 | [Hubot]: http://hubot.github.com
20 | [Jenkins]: http://jenkins-ci.org
21 | [w]: http://developer.github.com/v3/repos/hooks/
22 |
23 | Hubot usage
24 | -----------
25 |
26 | Start by setting up a new Jenkins job and GitHub web hook for a
27 | repository: `[ORG]/[REPO]`
28 |
29 | hubot ci setup github/janky
30 |
31 | The `setup` command can safely be run over and over again. It won't do
32 | anything unless it needs to. It takes an optional `name` argument: `[ORG]/[REPO] [NAME]`
33 |
34 | hubot ci setup github/janky janky-ruby1.9.2
35 |
36 | It also takes an optional `template` argument: `[ORG]/[REPO] [NAME] [TEMPLATE]`
37 |
38 | hubot ci setup github/janky janky-ruby1.9.2 ruby-build
39 |
40 | All branches are built automatically on push. Disable auto build with:
41 |
42 | hubot ci toggle [REPO]
43 |
44 | **NOTE**: If `name` was set you'll need to use it intested.
45 |
46 | hubot ci toggle [NAME]
47 |
48 | Run the command again to re-enable it. Force a build of the master
49 | branch:
50 |
51 | hubot ci build [REPO]
52 |
53 | **NOTE**: If `name` was set you'll need to use it intested.
54 |
55 | hubot ci build [NAME]
56 |
57 | Of a specific branch: `[REPO]/[BRANCH]`
58 |
59 | hubot ci build janky/libgit2
60 |
61 | Different builds aren't relevant to the same chat room and so Janky
62 | lets you choose where notifications are sent to. First get a list of
63 | available rooms:
64 |
65 | hubot ci rooms
66 |
67 | Then pick one:
68 |
69 | hubot ci set room janky The Serious Room
70 |
71 | Get the status of a build:
72 |
73 | hubot ci status janky
74 |
75 | Specific branch: `[REPO]/[BRANCH]`
76 |
77 | hubot ci status janky/libgit2
78 |
79 | All builds:
80 |
81 | hubot ci status
82 |
83 | Finally, get a quick reference of the available commands with:
84 |
85 | hubot ci?
86 |
87 | Installing
88 | ----------
89 |
90 | ### Jenkins
91 |
92 | Janky requires access to a Jenkins server. Version **1.580** is
93 | recommended. Refer to the Jenkins [documentation][doc] for installation
94 | instructions and install the [Notification Plugin][np] version 1.4.
95 |
96 | Remember to set the Jenkins URL in `http://your-jenkins-server.com/configure`.
97 | Janky will still trigger builds but will not update the build status without this set.
98 |
99 | [doc]: https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins
100 | [np]: https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin
101 |
102 | ### Deploying
103 |
104 | Janky is designed to be deployed to [Heroku](https://heroku.com).
105 |
106 | Grab all the necessary files from [the gist][gist]:
107 |
108 | $ git clone git://gist.github.com/1497335 janky
109 |
110 | Then push it up to a new Heroku app:
111 |
112 | $ cd janky
113 | $ heroku create --stack cedar
114 | $ bundle install
115 | $ git add Gemfile.lock
116 | $ git commit Gemfile.lock -m "lock bundle"
117 | $ git push heroku master
118 |
119 | After configuring the app (see below), create the database:
120 |
121 | $ heroku run rake db:migrate
122 |
123 | **NOTE:** Ruby version 2.0.0+ is required to run Janky.
124 |
125 | [gist]: https://gist.github.com/1497335
126 |
127 | Upgrading
128 | ---------
129 |
130 | We **strongly recommend** backing up your Janky database before upgrading.
131 |
132 | The general process is to then upgrade the gem, and then run migrate. Here is how
133 | you do that on a local box you have access to (this process will differ for Heroku):
134 |
135 | cd [PATH-TO-JANKY]
136 | gem update janky
137 | rake db:migrate
138 |
139 | Configuring
140 | -----------
141 |
142 | Janky is configured using environment variables. Use the `heroku config`
143 | command:
144 |
145 | $ heroku config:add VARIABLE=value
146 |
147 | Required settings:
148 |
149 | * `JANKY_BASE_URL`: The application URL **with** a trailing slash. Example:
150 | `http://mf-doom-42.herokuapp.com/`.
151 | * `JANKY_BUILDER_DEFAULT`: The Jenkins server URL **with** a trailing slash.
152 | Example: `http://jenkins.example.com/`. For basic auth, include the
153 | credentials in the URL: `http://user:pass@jenkins.example.com/`.
154 | Using GitHub OAuth with Jenkins is not supported by Janky.
155 | * `JANKY_CONFIG_DIR`: Directory where build config templates are stored.
156 | Typically set to `/app/config` on Heroku.
157 | * `JANKY_HUBOT_USER`: Login used to protect the Hubot API.
158 | * `JANKY_HUBOT_PASSWORD`: Password for the Hubot API.
159 | * `JANKY_GITHUB_USER`: The login of the GitHub user used to access the
160 | API. Requires Administrative privileges to set up service hooks.
161 | * `JANKY_GITHUB_PASSWORD`: The password for the GitHub user.
162 | * `JANKY_GITHUB_HOOK_SECRET`: Secret used to sign hook requests from
163 | GitHub.
164 | * `JANKY_CHAT_DEFAULT_ROOM`: Chat room where notifications are sent by default.
165 |
166 | Optional database settings:
167 |
168 | * `DATABASE_URL`: Database connection URL. Example:
169 | `postgres://user:password@host:port/db_name`.
170 | * `JANKY_DATABASE_SOCKET`: Path to the database socket. Example:
171 | `/var/run/mysql5/mysqld.sock`.
172 |
173 | ### GitHub Enterprise
174 |
175 | Using Janky with [GitHub Enterprise][ghe] requires one extra setting:
176 |
177 | * `JANKY_GITHUB_API_URL`: Full API URL of the instance, *with* a trailing
178 | slash. Example: `https://github.example.com/api/v3/`.
179 |
180 | [ghe]: https://enterprise.github.com
181 |
182 | ### GitHub Status API
183 |
184 | https://github.com/blog/1227-commit-status-api
185 |
186 | To update pull requests with the build status generate an OAuth token
187 | via the GitHub API:
188 |
189 | curl -u username:password \
190 | -d '{ "scopes": [ "repo:status" ], "note": "janky" }' \
191 | https://api.github.com/authorizations
192 |
193 | then set `JANKY_GITHUB_STATUS_TOKEN`. Optionally, you can also set
194 | `JANKY_GITHUB_STATUS_CONTEXT` to send a context to the GitHub API by
195 | default
196 |
197 | `username` and `password` in the above example should be the same as the
198 | values provided for `JANKY_GITHUB_USER` and `JANKY_GITHUB_PASSWORD`
199 | respectively.
200 |
201 | ### Chat notifications
202 |
203 | #### HipChat
204 |
205 | Required settings:
206 |
207 | * `JANKY_CHAT=hipchat`
208 | * `JANKY_CHAT_HIPCHAT_TOKEN`: authentication token (This token needs to be an
209 | admin token, not a notification token.)
210 | * `JANKY_CHAT_HIPCHAT_FROM`: name that messages will appear be sent from.
211 | Defaults to `CI`.
212 | * `JANKY_HUBOT_USER` should be XMPP/Jabber username in format xxxxx_xxxxxx
213 | rather than email
214 | * `JANKY_CHAT_DEFAULT_ROOM` should be the name of the room instead of the
215 | XMPP format, for example: `Engineers` instead of xxxx_xxxxxx.
216 |
217 | Installation:
218 |
219 | * Add `require "janky/chat_service/hipchat"` to the `config/environment.rb`
220 | file **before** the `Janky.setup(ENV)` line.
221 | * `echo 'gem "hipchat", "~>0.4"' >> Gemfile`
222 | * `bundle`
223 | * `git commit -am "install hipchat"`
224 |
225 | #### Slack
226 |
227 | Required settings:
228 |
229 | * `JANKY_CHAT=slack`
230 | * `JANKY_CHAT_SLACK_TEAM`: slack team name
231 | * `JANKY_CHAT_SLACK_TOKEN`: authentication token for the user sending build notifications.
232 | * `JANKY_CHAT_SLACK_USERNAME`: name that messages will appear be sent from.
233 | Defaults to `CI`.
234 | * `JANKY_CHAT_SLACK_ICON_URL`: URL to an image to use as the icon for this message.
235 |
236 | Installation:
237 |
238 | * Add `require "janky/chat_service/slack"` to the `config/environment.rb`
239 | file **before** the `Janky.setup(ENV)` line.
240 | * `echo 'gem "slack.rb"' >> Gemfile`
241 | * `bundle`
242 | * `git commit -am "install slack"`
243 |
244 | #### Hubot
245 |
246 | Sends notifications to Hubot via [janky script](http://git.io/hubot-janky).
247 |
248 | Required settings:
249 |
250 | * `JANKY_CHAT=hubot`
251 | * `JANKY_CHAT_HUBOT_URL`: URL to your Hubot instance.
252 | * `JANKY_CHAT_HUBOT_ROOMS`: List of rooms which can be set via `ci set room`.
253 | * For IRC: Comma-separated list of channels `"#room, #another-room"`
254 | * For Campfire/HipChat: List with room id and name `"34343:room, 23223:another-room"`
255 | * For Slack: List with room names `"room, another-room"`
256 |
257 | Installation:
258 | * Add `require "janky/chat_service/hubot"` to the `config/environment.rb`
259 | file **before** the `Janky.setup(ENV)` line.
260 |
261 | ### Authentication
262 |
263 | To restrict access to members of a GitHub organization, [register a new
264 | OAuth application on GitHub](https://github.com/settings/applications)
265 | with the callback set to `$JANKY_BASE_URL/auth/github/callback` then set
266 | a few extra settings:
267 |
268 | * `JANKY_SESSION_SECRET`: Random session cookie secret. Typically
269 | generated by a tool like `pwgen`.
270 | * `JANKY_AUTH_CLIENT_ID`: The client ID of the OAuth application.
271 | * `JANKY_AUTH_CLIENT_SECRET`: The client secret of the OAuth application.
272 | * `JANKY_AUTH_ORGANIZATION`: The organization name. Example: "github".
273 | * `JANKY_AUTH_TEAM_ID`: An optional team ID to give auth to. Example: "1234".
274 |
275 | ### Hubot
276 |
277 | Install the [janky script](http://git.io/hubot-janky) in your Hubot
278 | then set the `HUBOT_JANKY_URL` environment variable. Example:
279 | `http://user:password@janky.example.com/_hubot/`, with user and password
280 | replaced by `JANKY_HUBOT_USER` and `JANKY_HUBOT_PASSWORD` respectively.
281 |
282 | ### Custom build configuration
283 |
284 | The default build command should suffice for most Ruby applications:
285 |
286 | $ bundle install --path vendor/gems --binstubs
287 | $ bundle exec rake
288 |
289 | For more control you can add a `script/cibuild` at the root of your
290 | repository for Jenkins to execute instead.
291 |
292 | For total control, whole Jenkins' `config.xml` files can be associated
293 | with Janky builds. Given a build called `windows` and a template name
294 | of `psake`, Janky will try `config/jobs/psake.xml.erb` to use a template,
295 | `config/jobs/windows.xml.erb` to try the job name if the template does
296 | not exit, before finally falling back to the default
297 | configuration, `config/jobs/default.xml.erb`. After updating or adding
298 | a custom config, run `hubot ci setup` again to update the Jenkins
299 | server.
300 |
301 | Hacking
302 | -------
303 |
304 | Get your environment up and running:
305 |
306 | script/bootstrap
307 |
308 | Create the databases:
309 |
310 | mysqladmin -uroot create janky_development
311 | mysqladmin -uroot create janky_test
312 |
313 | Create the tables:
314 |
315 | RACK_ENV=development bin/rake db:migrate
316 | RACK_ENV=test bin/rake db:migrate
317 |
318 | Seed some data into the development database:
319 |
320 | bin/rake db:seed
321 |
322 | Start the server:
323 |
324 | script/server
325 |
326 | Open the app:
327 |
328 | open http://localhost:9393/
329 |
330 | Run the test suite:
331 |
332 | script/test
333 |
334 | Contributing
335 | ------------
336 |
337 | Fork the [Janky repository on GitHub](https://github.com/github/janky) and
338 | send a Pull Request. Note that any changes to behavior without tests will
339 | be rejected. If you are adding significant new features, please add both
340 | tests and documentation.
341 |
342 | Maintainers
343 | -----------
344 |
345 | * [@mattr-](https://github.com/mattr-)
346 |
347 | Copying
348 | -------
349 |
350 | Copyright © 2011-2014, GitHub, Inc. See the `COPYING` file for license
351 | rights and limitations (MIT).
352 |
--------------------------------------------------------------------------------