This commit is contained in:
Simon Rozet 2011-12-19 15:23:59 +01:00
Коммит bb1b0b90b7
71 изменённых файлов: 3780 добавлений и 0 удалений

6
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,6 @@
.bundle
bin
vendor/gems
Gemfile.lock
*.gem
.rbenv-version

22
COPYING Normal file
Просмотреть файл

@ -0,0 +1,22 @@
Copyright (c) 2011 GitHub, Inc. <https://github.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

2
Gemfile Normal file
Просмотреть файл

@ -0,0 +1,2 @@
source "http://rubygems.org"
gemspec

211
README.md Normal file
Просмотреть файл

@ -0,0 +1,211 @@
Janky
=====
This is Janky, a continuous integration server built on top of
[Jenkins][], controlled by [Hubot][], and designed for [GitHub][].
* **Built on top of Jenkins.** The power, vast amount of plugins and large
communauty of the popular CI server all wrapped up in a great experience.
* **Controlled by Hubot.** Day to day operations are exposed as simple
Hubot commands that the whole team can use.
* **Designed for GitHub.** Janky creates the appropriate [web hooks][w] for
you and the web app restricts access to members of your GitHub organization.
[GitHub]: https://github.com
[Hubot]: http://hubot.github.com
[Jenkins]: http://jenkins-ci.org
[w]: http://developer.github.com/v3/repos/hooks/
Hubot Usage
-----------
Start by setting up a new Jenkins job and GitHub web hook for a
repository:
hubot ci setup github/janky
The `setup` command can safely be run over and over again. It won't do
anything unless it needs to. It takes an optional name argument:
hubot ci setup github/janky janky-ruby1.9.2
All branches are built automatically on push. Disable auto build with:
hubot ci toggle janky
Run the command again to re-enable it. Force a build of the master
branch:
hubot ci build janky
Of a specific branch:
hubot ci build janky/libgit2
Different builds aren't relevant to the same Campfire room and so Janky
lets you choose where notifications are sent to. First get a list of
available rooms:
hubot ci rooms
Then pick one:
hubot ci set janky room The Serious Room
Get the status of a build:
hubot ci status janky
Specific branch:
hubot ci status janky/libgit2
All builds:
hubot ci status
Finally, get a quick reference of the available commands with:
hubot ci?
Installing
----------
### Jenkins
Janky requires access to a Jenkins server. Version **1.427** is
recommended. Refer to the Jenkins [documentation][doc] for installation
instructions and install the [Notification Plugin][np] version 1.4.
[doc]: https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins
[np]: https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin
### Deploying
Janky is designed to be deployed to [Heroku](https://heroku.com).
Grab all the necessary files from [this gist][gist]:
$ git clone gist://gist.github.com/123 janky
Then push up it to a new Heroku app:
$ cd janky
$ heroku create --stack cedar
$ git push heroku master
After configuration the app (see below), create the database:
$ heroku run rake db:migrate
[gist]: https://gist.github.com/gist/1234
### Configuring
Janky is configured using environment variables. Use the `heroku config`
command:
$ heroku config:add VARIABLE=value
Required settings:
* `JANKY_BASE_URL`: The application URL with a trailing slash. Example:
`http://mf-doom-42.heroku.com/`.
* `JANKY_BUILDER_DEFAULT`: The Jenkins server URL with a trailing slash.
Example: `http://jenkins.example.com/`.
* `JANKY_CONFIG_DIR`: Directory where build config templates are stored.
Typically set to `/app/config` on Heroku.
* `JANKY_HUBOT_USER`: Login used to protect the Hubot API.
* `JANKY_HUBOT_PASSWORD`: Password for the Hubot API.
* `JANKY_GITHUB_USER`: The login of the GitHub user used to access the
API. Requires Push and Pull privileges.
* `JANKY_GITHUB_PASSWORD`: The password for the GitHub user.
* `JANKY_GITHUB_HOOK_SECRET`: Secret used to sign hook requests from
GitHub.
* `JANKY_CAMPFIRE_ACCOUNT`: The name of your Campfire account.
* `JANKY_CAMPFIRE_TOKEN`: The authentication token of the user sending
build notifications.
* `JANKY_CAMPFIRE_DEFAULT_ROOM`: The name of the room where notifications
are sent by default. Example: "Builds".
To restrict access to members of a GitHub organization, [register a new
OAuth application on GitHub](https://github.com/account/applications)
with the callback set to `$JANKY_BASE_URL/auth/github/callback` then set
a few extra settings:
* `JANKY_SESSION_SECRET`: Random session cookie secret. Typically
generated by a tool like `pgwen`.
* `JANKY_AUTH_CLIENT_ID`: The client ID of the OAuth application.
* `JANKY_AUTH_CLIENT_SECRET`: The client secret of the OAuth application.
* `JANKY_AUTH_ORGANIZATION`: The organization name. Example: "github".
### Hubot
Install the [ci script](http://git.io/hubot-ci-master) in your Hubot
then set the `HUBOT_JANKY_URL` environment variable. Example:
`http://user:secret@janky.example.com/_hubot/`, with user and password
replaced by `JANKY_HUBOT_USER` and `JANKY_HUBOT_PASSWORD` repectively.
### Custom Build Configuration
The default build command should suffice for most Ruby applications:
$ bundle install --path vendor/gems --binstubs
$ bundle exec rake
For more control you can add a `script/cibuild` at the root of your
repository for Jenkins to execute instead.
For total control, whole Jenkins' `config.xml` files can be associated
with Janky builds. Given a build called `windows`, Janky will try
`config/jobs/windows.xml.erb` before falling back to the default
configuration, `config/jobs/default.xml.erb`. After updating or adding
a custom config, run `hubot ci setup` again to update the Jenkins
server.
Hacking
-------
Create the databases:
$ mysqladmin -uroot create janky_development
$ mysqladmin -uroot create janky_test
Create the tables:
$ RACK_ENV=development bin/rake db:migrate
$ RACK_ENV=test bin/rake db:migrate
Get your environment up and running:
$ script/boostrap
Seed some data into the development database:
$ bin/rake db:seed
Start the server:
$ script/server
Open the app:
$ open http://localhost:9393/
Run the test suite:
$ ruby test/janky_spec.rb
Contributing
------------
Fork the [Janky repository on GitHub](https://github.com/github/janky) and
send a Pull Request.
Copying
-------
Copyright © 2011, GitHub, Inc. See the `COPYING` file for license
rights and limitations (MIT).

19
Rakefile Normal file
Просмотреть файл

@ -0,0 +1,19 @@
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
ENV["RACK_ENV"] ||= "development"
require "janky"
Janky.setup(ENV)
require "janky/tasks"
task "db:seed" do
if ENV["RACK_ENV"] != "development"
fail "refusing to load seed data into non-development database"
end
dump = File.expand_path("../lib/janky/database/seed.dump.gz", __FILE__)
Replicate::Loader.new do |loader|
loader.log_to $stderr, false, false
loader.read Zlib::GzipReader.open(dump)
end
end

3
config.ru Normal file
Просмотреть файл

@ -0,0 +1,3 @@
require "janky"
Janky.setup(ENV)
run Janky.app

101
janky.gemspec Normal file
Просмотреть файл

@ -0,0 +1,101 @@
require File.expand_path("../lib/janky/version", __FILE__)
Gem::Specification.new "janky", Janky::VERSION do |s|
s.description = "Janky is a Continuous Integration server"
s.summary = "Continuous Integration server built on top of Jenkins and " \
"designed for GitHub and Hubot"
s.authors = ["GitHub, Inc."]
s.homepage = "https://github.com/github/janky"
s.has_rdoc = false
# runtime
s.add_dependency "rake", "~>0.9.2"
s.add_dependency "sinatra", "~>1.3"
s.add_dependency "sinatra_auth_github", "~>0.1.5"
s.add_dependency "mustache", "~>0.11"
s.add_dependency "yajl-ruby", "~>0.8"
s.add_dependency "activerecord", "~>3.1.0"
s.add_dependency "broach", "~>0.2"
s.add_dependency "replicate", "~>1.4"
# development
s.add_development_dependency "shotgun", "~>0.9"
s.add_development_dependency "thin", "~>1.2"
s.add_development_dependency "mysql2", "~>0.3.0"
# test
s.add_development_dependency "database_cleaner", "~>0.6"
s.files = %w[
COPYING
Gemfile
README.md
Rakefile
config.ru
janky.gemspec
lib/janky.rb
lib/janky/app.rb
lib/janky/branch.rb
lib/janky/build.rb
lib/janky/build_request.rb
lib/janky/builder.rb
lib/janky/builder/client.rb
lib/janky/builder/http.rb
lib/janky/builder/mock.rb
lib/janky/builder/payload.rb
lib/janky/builder/receiver.rb
lib/janky/builder/runner.rb
lib/janky/campfire.rb
lib/janky/campfire/mock.rb
lib/janky/commit.rb
lib/janky/database/migrate/1312115512_init.rb
lib/janky/database/migrate/1312117285_non_unique_repo_uri.rb
lib/janky/database/migrate/1312198807_repo_enabled.rb
lib/janky/database/migrate/1313867551_add_build_output_column.rb
lib/janky/database/migrate/1313871652_add_commit_url_column.rb
lib/janky/database/migrate/1317384618_add_repo_hook_url.rb
lib/janky/database/migrate/1317384619_add_build_room_id.rb
lib/janky/database/migrate/1317384629_drop_default_room_id.rb
lib/janky/database/migrate/1317384649_github_team_id.rb
lib/janky/database/schema.rb
lib/janky/database/seed.dump.gz
lib/janky/exception.rb
lib/janky/github.rb
lib/janky/github/api.rb
lib/janky/github/commit.rb
lib/janky/github/mock.rb
lib/janky/github/payload.rb
lib/janky/github/payload_parser.rb
lib/janky/github/receiver.rb
lib/janky/helpers.rb
lib/janky/hubot.rb
lib/janky/job_creator.rb
lib/janky/notifier.rb
lib/janky/notifier/campfire.rb
lib/janky/notifier/mock.rb
lib/janky/notifier/multi.rb
lib/janky/public/css/base.css
lib/janky/public/images/building-bot.gif
lib/janky/public/images/disclosure-arrow.png
lib/janky/public/images/logo.png
lib/janky/public/images/robawt-status.gif
lib/janky/public/javascripts/application.js
lib/janky/public/javascripts/jquery.js
lib/janky/public/javascripts/jquery.relatize.js
lib/janky/repository.rb
lib/janky/tasks.rb
lib/janky/templates/console.mustache
lib/janky/templates/index.mustache
lib/janky/templates/layout.mustache
lib/janky/version.rb
lib/janky/views/console.rb
lib/janky/views/index.rb
lib/janky/views/layout.rb
]
s.test_files = %w[
test/default.xml.erb
test/janky_test.rb
test/test_helper.rb
]
end

224
lib/janky.rb Normal file
Просмотреть файл

@ -0,0 +1,224 @@
require "net/http"
require "digest/md5"
require "active_record"
require "replicate"
require "sinatra/base"
require "mustache/sinatra"
require "yajl"
require "yajl/json_gem"
require "tilt"
require "broach"
require "sinatra/auth/github"
require "janky/repository"
require "janky/branch"
require "janky/commit"
require "janky/build"
require "janky/build_request"
require "janky/github"
require "janky/github/api"
require "janky/github/mock"
require "janky/github/payload"
require "janky/github/commit"
require "janky/github/payload_parser"
require "janky/github/receiver"
require "janky/job_creator"
require "janky/helpers"
require "janky/hubot"
require "janky/builder"
require "janky/builder/client"
require "janky/builder/runner"
require "janky/builder/http"
require "janky/builder/mock"
require "janky/builder/payload"
require "janky/builder/receiver"
require "janky/campfire"
require "janky/exception"
require "janky/notifier"
require "janky/notifier/mock"
require "janky/notifier/multi"
require "janky/notifier/campfire"
require "janky/app"
require "janky/views/layout"
require "janky/views/index"
require "janky/views/console"
# This is Janky, a continuous integration server. Checkout the 'app'
# method on this module for an overview of the different components
# involved.
module Janky
# The base exception class raised when errors are encountered.
class Error < StandardError; end
# Setup the application, including the database and Jenkins connections.
#
# settings - Hash of app settings. Typically ENV but any object responding
# to #[] is valid. See required_settings for required keys.
# The RACK_ENV setting is always required.
#
# Raises an Error when required settings are missing.
# Returns nothing.
def self.setup(settings)
env = settings["RACK_ENV"]
if env.nil? || env.empty?
raise Error, "RACK_ENV is required"
end
required_settings.each do |setting|
next if !settings[setting].nil? && !settings[setting].empty?
if env == "production"
raise Error, "#{setting} setting is required"
end
end
if env != "production"
settings["DATABASE_URL"] ||= "mysql2://root@localhost/janky_#{env}"
settings["JANKY_BASE_URL"] ||= "http://localhost:9393"
settings["JANKY_BUILDER_DEFAULT"] ||= "http://localhost:8080/"
settings["JANKY_CONFIG_DIR"] ||= File.dirname(__FILE__)
end
database = URI(settings["DATABASE_URL"])
adapter = database.scheme == "postgres" ? "postgresql" : database.scheme
base_url = URI(settings["JANKY_BASE_URL"]).to_s
ActiveRecord::Base.establish_connection(
:adapter => adapter,
:host => database.host,
:database => database.path[1..-1],
:username => database.user,
:password => database.password,
:reconnect => true
)
self.jobs_config_dir = config_dir = Pathname(settings["JANKY_CONFIG_DIR"])
if !config_dir.directory?
raise Error, "#{config_dir} is not a directory"
end
# Setup the callback URL of this Janky host.
Janky::Builder.setup(base_url + "/_builder")
# Setup the default Jenkins build host
Janky::Builder[:default] = settings["JANKY_BUILDER_DEFAULT"]
Janky::GitHub.setup(
settings["JANKY_GITHUB_USER"],
settings["JANKY_GITHUB_PASSWORD"],
settings["JANKY_GITHUB_HOOK_SECRET"],
base_url + "/_github"
)
if settings.key?("JANKY_SESSION_SECRET")
Janky::App.register Sinatra::Auth::Github
Janky::App.set({
:sessions => true,
:session_secret => settings["JANKY_SESSION_SECRET"],
:github_team_id => settings["JANKY_AUTH_TEAM_ID"],
:github_organization => settings["JANKY_AUTH_ORGANIZATION"],
:github_options => {
:secret => settings["JANKY_AUTH_CLIENT_SECRET"],
:client_id => settings["JANKY_AUTH_CLIENT_ID"],
:scopes => "repo",
},
})
end
Janky::Hubot.set(
:base_url => settings["JANKY_BASE_URL"],
:username => settings["JANKY_HUBOT_USER"],
:password => settings["JANKY_HUBOT_PASSWORD"]
)
Janky::Campfire.setup(
settings["JANKY_CAMPFIRE_ACCOUNT"],
settings["JANKY_CAMPFIRE_TOKEN"],
settings["JANKY_CAMPFIRE_DEFAULT_ROOM"]
)
Janky::Exception.setup(Janky::Exception::Mock)
Notifier.setup(Notifier::Campfire)
end
# List of settings required in production.
#
# Returns an Array of Strings.
def self.required_settings
%w[RACK_ENV DATABASE_URL
JANKY_BASE_URL
JANKY_BUILDER_DEFAULT
JANKY_CONFIG_DIR
JANKY_GITHUB_USER JANKY_GITHUB_PASSWORD JANKY_GITHUB_HOOK_SECRET
JANKY_HUBOT_USER JANKY_HUBOT_PASSWORD
JANKY_CAMPFIRE_ACCOUNT JANKY_CAMPFIRE_TOKEN JANKY_CAMPFIRE_DEFAULT_ROOM]
end
# Directory where Jenkins job configuration templates are located.
#
# Returns the directory as a Pathname.
class << self
attr_accessor :jobs_config_dir
end
# Mock out all network-dependant components. Must be called after setup.
# Typically used in test environments.
#
# Returns nothing.
def self.enable_mock!
Janky::Builder.enable_mock!
Janky::GitHub.enable_mock!
Janky::Notifier.enable_mock!
Janky::Campfire.enable_mock!
Janky::App.disable :github_team_id
end
# Reset the state of the mocks.
#
# Returns nothing.
def self.reset!
Janky::Notifier.reset!
Janky::Builder.reset!
end
# The Janky Rack application, assembled from four apps. Exceptions
# raised during the request cycle are caught by the Exception
# middleware which typically report exceptions to an external
# service before re-raising the exception.
#
# Returns a memoized Rack application.
def self.app
@app ||= Rack::Builder.app {
# Exception reporting middleware.
use Janky::Exception::Middleware
# GitHub Post-Receive requests.
map "/_github" do
run Janky::GitHub.receiver
end
# Jenkins callback requests.
map "/_builder" do
run Janky::Builder.receiver
end
# Hubot API, protected by Basic Auth.
map "/_hubot" do
use Rack::Auth::Basic do |username, password|
username == Janky::Hubot.username &&
password == Janky::Hubot.password
end
run Janky::Hubot
end
# Web dashboard
map "/" do
use Janky::NoAuth
run Janky::App
end
}
end
end

81
lib/janky/app.rb Normal file
Просмотреть файл

@ -0,0 +1,81 @@
module Janky
class App < Sinatra::Base
register Mustache::Sinatra
register Helpers
set :app_file, __FILE__
enable :static
set :mustache, {
:namespace => Janky,
:views => File.join(root, "views"),
:templates => File.join(root, "templates")
}
before do
if organization = github_organization
github_organization_authenticate!(organization)
end
end
def github_organization
settings.respond_to?(:github_organization) && settings.github_organization
end
def github_team_id
settings.respond_to?(:github_team_id) && settings.github_team_id
end
def authorize_index
if github_team_id
github_team_authenticate!(github_team_id)
end
end
def authorize_repo(repo)
if team_id = (repo.github_team_id || github_team_id)
github_team_authenticate!(team_id)
end
end
get "/?" do
authorize_index
@builds = Build.started.first(50)
mustache :index
end
get "/:build_id/output" do |build_id|
@build = Build.find(build_id)
authorize_repo(@build.repo)
mustache :console, :layout => false
end
get "/:repo_name" do |repo_name|
repo = find_repo(repo_name)
authorize_repo(repo)
@builds = repo.builds.started.first(50)
mustache :index
end
get "/:repo_name/:branch" do |repo_name, branch|
repo = find_repo(repo_name)
authorize_repo(repo)
@builds = repo.branch_for(branch).builds.started.first(50)
mustache :index
end
end
class NoAuth < Sinatra::Base
register Helpers
get "/boomtown" do
raise Error, "boomtown"
end
get "/site/sha" do
`cd /data/janky && git rev-parse HEAD`
end
end
end

112
lib/janky/branch.rb Normal file
Просмотреть файл

@ -0,0 +1,112 @@
module Janky
class Branch < ActiveRecord::Base
belongs_to :repository
has_many :builds
# Is this branch green?
#
# Returns a Boolean.
def green?
if current_build
current_build.green?
end
end
# Is this branch red?
#
# Returns a Boolean.
def red?
if current_build
current_build.red?
end
end
# Is this branch building?
#
# Returns a Boolean.
def building?
if current_build
current_build.building?
end
end
# Is this branch completed?
#
# Returns a Boolean.
def completed?
if current_build
current_build.completed?
end
end
# Find all completed builds, sorted by completion date, most recent first.
#
# Returns an Array of Builds.
def completed_builds
builds.completed
end
# Create a build for the given commit.
#
# commit - the Janky::Commit instance to build.
# compare - optional String GitHub Compare View URL. Defaults to the
# commit last build, if any.
# room_id - optional Fixnum Campfire room ID. Defaults to the room set on
# the repository.
#
# Returns the newly created Janky::Build.
def build_for(commit, room_id = nil, compare = nil)
if compare.nil? && build = commit.last_build
compare = build.compare
end
if room_id.nil? || room_id.zero?
room_id = repository.room_id
end
builds.create(
:compare => compare,
:commit => commit,
:room_id => room_id
)
end
# The current build, e.g. the most recent one.
#
# Returns a Build.
def current_build
builds.last
end
# Human readable status of this branch
#
# Returns a String.
def status
if current_build && current_build.building?
"building"
elsif build = completed_builds.first
if build.green?
"green"
elsif build.red?
"red"
end
elsif completed_builds.empty? || builds.empty?
"no build"
else
raise Error, "unexpected branch status: #{id.inspect}"
end
end
# Hash representation of this branch status.
#
# Returns a Hash with the name, status, sha1 and compare url.
def to_hash
{
:name => repository.name,
:status => status,
:sha1 => (current_build && current_build.sha1),
:compare => (current_build && current_build.compare)
}
end
end
end

223
lib/janky/build.rb Normal file
Просмотреть файл

@ -0,0 +1,223 @@
module Janky
class Build < ActiveRecord::Base
belongs_to :branch
belongs_to :commit
# Transition the Build to the started state.
#
# id - the Fixnum ID used to find the build.
# url - the full String URL of the build.
#
# Returns nothing or raises an Error for inexistant builds.
def self.start(id, url)
if build = find_by_id(id)
build.start(url, Time.now)
else
raise Error, "Unknown build: #{id.inspect}"
end
end
# Transition the Build to the completed state.
#
# id - the Fixnum ID used to find the build.
# green - Boolean indicating build success.
#
# Returns nothing or raises an Error for inexistant builds.
def self.complete(id, green)
if build = find_by_id(id)
build.complete(green, Time.now)
else
raise Error, "Unknown build: #{id.inspect}"
end
end
# Find all started builds, most recent first.
#
# Returns an Array of Builds.
def self.started
where("started_at IS NOT NULL").order("started_at DESC")
end
# Find all completed builds, most recent first.
#
# Returns an Array of Builds.
def self.completed
started.
where("completed_at IS NOT NULL")
end
# Find all green builds, most recent first.
#
# Returns an Array of Builds.
def self.green
completed.where(:green => true)
end
# Is this build currently being built?
#
# Returns a Boolean.
def building?
started? && !completed?
end
# Is this build red?
#
# Returns a Boolean, nothing when the build hasn't completed yet.
def red?
completed? && !green?
end
# Was this build ever started?
#
# Returns a Boolean.
def started?
! started_at.nil?
end
# Did this build complete?
#
# Returns a Boolean.
def completed?
! completed_at.nil?
end
# Trigger a Jenkins build using the appropriate builder.
#
# Returns nothing.
def run
builder.run(self)
end
# See Repository#builder.
def builder
branch.repository.builder
end
# Run a copy of itself. Typically used to force a build in case of
# temporary test failure or when auto-build is disabled.
#
# new_room_id - optional Campfire room Fixnum ID. Defaults to the room of the
# build being re-run.
#
# Returns the build copy.
def rerun(new_room_id = nil)
build = branch.build_for(commit, new_room_id)
build.run
build
end
# Cached or remote build output.
#
# Returns the String output.
def output
if completed?
read_attribute(:output)
elsif started?
output_remote
else
""
end
end
# Retrieve the build output from the Jenkins server.
#
# Returns the String output.
def output_remote
if started?
builder.output(self)
end
end
# Mark the build as started.
#
# url - the full String URL of the build on the Jenkins server.
# now - the Time at which the build started.
#
# Returns nothing or raise an Error for weird transitions.
def start(url, now)
if started?
raise Error, "Build #{id} already started"
elsif completed?
raise Error, "Build #{id} already completed"
else
update_attributes!(:url => url, :started_at => now)
Notifier.started(self)
end
end
# Mark the build as complete, store the build output and notify Campfire.
#
# green - Boolean indicating build success.
# now - the Time at which the build completed.
#
# Returns nothing or raise an Error for weird transitions.
def complete(green, now)
if ! started?
raise Error, "Build #{id} not started"
elsif completed?
raise Error, "Build #{id} already completed"
else
update_attributes!(
:green => green,
:completed_at => now,
:output => output_remote
)
Notifier.completed(self)
end
end
# The time it took to peform this build in seconds.
#
# Returns an Integer seconds.
def duration
if completed?
Integer(completed_at - started_at)
end
end
# The name of the Campfire room where notifications are sent.
#
# Returns the String room name.
def room_name
if room_id && room_id > 0
Campfire.room_name(room_id)
end
end
def repo_id
repository.id
end
def repo_job_name
repository.job_name
end
def repo_name
repository.name
end
def repository
branch.repository
end
def repo
branch.repository
end
def sha1
commit.short_sha
end
def commit_url
commit.url
end
def number
id.to_s
end
def branch_name
branch.name
end
end
end

Просмотреть файл

@ -0,0 +1,49 @@
module Janky
class BuildRequest
def self.handle(repo_uri, branch_name, commit, compare, room_id)
repos = Repository.find_all_by_uri(repo_uri)
repos.each do |repo|
begin
new(repo, branch_name, commit, compare, room_id).handle
rescue Janky::Error => boom
Exception.report(boom, :repo => repo.name)
end
end
repos.size
end
def initialize(repo, branch_name, commit, compare, room_id)
@repo = repo
@branch_name = branch_name
@commit = commit
@compare = compare
@room_id = room_id
end
def handle
current_build = commit.last_build
build = branch.build_for(commit, @room_id, @compare)
if !current_build || (current_build && current_build.red?)
if @repo.enabled?
build.run
end
end
end
def branch
@repo.branch_for(@branch_name)
end
def commit
@repo.commit_for(
:sha1 => @commit.sha1,
:url => @commit.url,
:message => @commit.message,
:author => @commit.author,
:committed_at => @commit.committed_at
)
end
end
end

108
lib/janky/builder.rb Normal file
Просмотреть файл

@ -0,0 +1,108 @@
module Janky
# Triggers Jenkins builds and handles callbacks.
#
# The HTTP requests flow goes like this:
#
# 1. Send a Build request to the Jenkins server over HTTP. The resulting
# build URL is stored in Build#url.
#
# 2. Once Jenkins picks up the build and starts running it, it sends a callback
# handled by the `receiver` Rack app, which transitions the build into a
# building state.
#
# 3. Finally, Jenkins sends another callback with the build result and the
# build is transitioned to a completed and green/red state.
#
# The Mock adapter provides methods to simulate that flow without having to
# go over the wire.
module Builder
# Set the callback URL of builder clients. Must be called before
# registering any client.
#
# callback_url - The absolute callback URL as a String.
#
# Returns nothing.
def self.setup(callback_url)
@callback_url = callback_url
end
# Public: Define the rule for picking a builder.
#
# block - Required block that will be given a Repository object when
# picking a builder. Must return a Client object.
#
# Returns nothing.
def self.choose(&block)
@chooser = block
end
# Pick the appropriate builder for a repo based on the rule set by the
# choose method. Uses the default builder when no rule is defined.
#
# repo - a Repository object.
#
# Returns a Client object.
def self.pick_for(repo)
if block = @chooser
block.call(repo)
else
self[:default]
end
end
# Register a new build host.
#
# url - The String URL of the Jenkins server.
#
# Returns the new Client instance.
def self.[]=(builder, url)
builders[builder] = Client.new(url, @callback_url)
end
# Get the Client for a registered build host.
#
# builder - the String name of the build host.
#
# Returns the Client instance.
def self.[](builder)
builders[builder] ||
raise(Error, "Unknown builder: #{builder.inspect}")
end
# Registered build hosts.
#
# Returns an Array of Client.
def self.builders
@builders ||= {}
end
# Rack app handling HTTP callbacks coming from the Jenkins server.
def self.receiver
@receiver ||= Janky::Builder::Receiver
end
def self.enable_mock!
builders.values.each { |b| b.enable_mock! }
end
def self.green!
builders.values.each { |b| b.green! }
end
def self.red!
builders.values.each { |b| b.red! }
end
def self.reset!
builders.values.each { |b| b.reset! }
end
def self.start!
builders.values.each { |b| b.start! }
end
def self.complete!
builders.values.each { |b| b.complete! }
end
end
end

Просмотреть файл

@ -0,0 +1,82 @@
module Janky
module Builder
class Client
def initialize(url, callback_url)
@url = URI(url)
@callback_url = URI(callback_url)
end
# The String absolute URL of the Jenkins server.
attr_reader :url
# The String absoulte URL callback of this Janky host.
attr_reader :callback_url
# Trigger a Jenkins build for the given Build.
#
# build - a Build object.
#
# Returns the Jenkins build URL.
def run(build)
Runner.new(@url, build, adapter).run
end
# Retrieve the output of the given Build.
#
# build - a Build object. Must have an url attribute.
#
# Returns the String build output.
def output(build)
Runner.new(@url, build, adapter).output
end
# Setup a job on the Jenkins server.
#
# name - The desired job name as a String.
# repo_uri - The repository git URI as a String.
# template_path - The Pathname to the XML config template.
#
# Returns nothing.
def setup(name, repo_uri, template_path)
job_creator.run(name, repo_uri, template_path)
end
# The adapter used to trigger builds. Defaults to HTTP, which hits the
# Jenkins server configured by `setup`.
def adapter
@adapter ||= HTTP.new(url.user, url.password)
end
def job_creator
@job_creator ||= JobCreator.new(url, @callback_url)
end
# Enable the mock adapter and make subsequent builds green.
def green!
@adapter = Mock.new(true, Janky.app)
job_creator.enable_mock!
end
# Alias green! as enable_mock!
alias_method :enable_mock!, :green!
# Alias green! as reset!
alias_method :reset!, :green!
# Enable the mock adapter and make subsequent builds red.
def red!
@adapter = Mock.new(false, Janky.app)
end
# Simulate the first callback. Only available when mocked.
def start!
@adapter.start
end
# Simulate the last callback. Only available when mocked.
def complete!
@adapter.complete
end
end
end
end

43
lib/janky/builder/http.rb Normal file
Просмотреть файл

@ -0,0 +1,43 @@
module Janky
module Builder
class HTTP
def initialize(username, password)
@username = username
@password = password
end
def run(params, create_url)
http = Net::HTTP.new(create_url.host, create_url.port)
request = Net::HTTP::Post.new(create_url.path)
if @username && @password
request.basic_auth(@username, @password)
end
request.form_data = {"json" => params}
response = http.request(request)
unless response.code == "302"
Exception.push_http_response(response)
raise Error, "Failed to create build"
end
end
def output(url)
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Get.new(url.path)
if @username && @password
request.basic_auth(@username, @password)
end
response = http.request(request)
unless response.code == "200"
Exception.push_http_response(response)
raise Error, "Failed to get build output"
end
response.body
end
end
end
end

45
lib/janky/builder/mock.rb Normal file
Просмотреть файл

@ -0,0 +1,45 @@
module Janky
module Builder
class Mock
def initialize(green, app)
@green = green
@app = app
@builds = []
end
def run(params, create_url)
params = Yajl.load(params)["parameter"]
param = params.detect{ |p| p["name"] == "JANKY_ID" }
build_id = param["value"]
url = create_url.to_s.gsub("build", build_id.to_s)
@builds << [build_id, "#{url}/", @green]
end
def output(build)
"....FFFUUUUUUU"
end
def start
@builds.each do |id, url, _|
payload = Payload.start(id, url)
request(payload)
end
end
def complete
@builds.each do |id, _, green|
payload = Payload.complete(id, green)
request(payload)
end
@builds.clear
end
def request(payload)
Rack::MockRequest.new(@app).post("/_builder",
:input => payload.to_json
)
end
end
end
end

Просмотреть файл

@ -0,0 +1,63 @@
module Janky
module Builder
class Payload
def self.parse(json)
parsed = Yajl.load(json)
build = parsed["build"]
new(
build["phase"],
build["parameters"]["JANKY_ID"],
build["full_url"],
build["status"]
)
end
def self.start(id, url)
new("STARTED", id, url, nil)
end
def self.complete(id, green)
status = (green ? "SUCCESS" : "FAILED")
new("FINISHED", id, nil, status)
end
def initialize(phase, id, url, status)
@phase = phase
@id = id
@url = url
@status = status
end
attr_reader :id, :url
def started?
@phase == "STARTED"
end
def completed?
@phase == "FINISHED"
end
def green?
if completed?
@status == "SUCCESS"
else
false
end
end
def to_json
{ :build => {
:phase => @phase,
:status => @status,
:full_url => @url,
:parameters => {
"JANKY_ID" => @id
}
}
}.to_json
end
end
end
end

Просмотреть файл

@ -0,0 +1,20 @@
module Janky
module Builder
class Receiver
def self.call(env)
request = Rack::Request.new(env)
payload = Payload.parse(request.body)
if payload.started?
Build.start(payload.id, payload.url)
elsif payload.completed?
Build.complete(payload.id, payload.green?)
else
return Rack::Response.new("Invalid", 402).finish
end
Rack::Response.new("OK", 201).finish
end
end
end
end

Просмотреть файл

@ -0,0 +1,47 @@
module Janky
module Builder
class Runner
def initialize(base_url, build, adapter)
@base_url = base_url
@build = build
@adapter = adapter
end
def run
context_push
@adapter.run(json_params, create_url)
end
def output
context_push
@adapter.output(output_url)
end
def json_params
Yajl.dump(:parameter => [
{ :name => "JANKY_SHA1", :value => @build.sha1 },
{ :name => "JANKY_ID", :value => @build.id }
])
end
def output_url
URI(@build.url + "consoleText")
end
def create_url
URI("#{@base_url}job/#{@build.repo_job_name}/build")
end
def context_push
Exception.push(
:base_url => @base_url.inspect,
:build => @build.inspect,
:adapter => @adapter.inspect,
:params => json_params.inspect,
:create_url => create_url.inspect
)
end
end
end
end

127
lib/janky/campfire.rb Normal file
Просмотреть файл

@ -0,0 +1,127 @@
module Janky
# Sends messages to Campfire and accesses available rooms.
module Campfire
# Setup the Campfire client with the given credentials.
#
# account - the Campfire account name as a String.
# token - the Campfire API token as a String.
# default - the name of the default Campfire room as a String.
#
# Returns nothing.
def self.setup(account, token, default)
::Broach.settings = {
"account" => account,
"token" => token,
"use_ssl" => true
}
self.default_room_name = default
end
class << self
attr_accessor :default_room_name
end
def self.default_room_id
room_id(default_room_name)
end
# Send a message to a Campfire room.
#
# message - The String message.
# room_id - The Integer room ID.
#
# Returns nothing.
def self.speak(message, room_id)
adapter.speak(room_name(room_id), message)
end
# Get the ID of a room.
#
# slug - the String name of the room.
#
# Returns the room ID or nil for unknown rooms.
def self.room_id(name)
if room = rooms.detect { |room| room.name == name }
room.id
end
end
# Get the name of a room given its ID.
#
# id - the Fixnum room ID.
#
# Returns the name as a String or nil when not found.
def self.room_name(id)
if room = rooms.detect { |room| room.id.to_s == id.to_s }
room.name
end
end
# Get a list of all rooms names.
#
# Returns an Array of room name as Strings.
def self.room_names
rooms.map { |room| room.name }.sort
end
# Memoized list of available rooms.
#
# Returns an Array of Broach::Room objects.
def self.rooms
@rooms ||= adapter.rooms
end
# Enable mocking. Once enabled, messages are discarded.
#
# Returns nothing.
def self.enable_mock!
@adapter = Mock.new
end
# Configure available rooms. Only available in mock mode.
#
# value - Hash of room map (Fixnum ID => String name)
#
# Returns nothing.
def self.rooms=(value)
adapter.rooms = value
end
def self.adapter
@adapter ||= Broach.new
end
class Broach
def speak(room_name, message)
::Broach.speak(room_name, message)
end
def rooms
::Broach.rooms
end
end
class Mock
def initialize
@rooms = {}
end
attr_writer :rooms
def speak(room_name, message)
if !@rooms.values.include?(room_name)
raise Error, "Unknown room #{room_name.inspect}"
end
end
def rooms
acc = []
@rooms.each do |id, name|
acc << ::Broach::Room.new("id" => id, "name" => name)
end
acc
end
end
end
end

Просмотреть файл

14
lib/janky/commit.rb Normal file
Просмотреть файл

@ -0,0 +1,14 @@
module Janky
class Commit < ActiveRecord::Base
belongs_to :repository
has_many :builds
def last_build
builds.last
end
def short_sha
sha1[0..7]
end
end
end

71
lib/janky/dashboard.rb Normal file
Просмотреть файл

@ -0,0 +1,71 @@
module Janky
class Dashboard < Sinatra::Base
register Mustache::Sinatra
register Helpers
set :app_file, __FILE__
enable :static
set :mustache, {
:namespace => Janky,
:views => File.join(root, "views"),
:templates => File.join(root, "templates")
}
def github_team_id
settings.respond_to?(:github_team_id) && settings.github_team_id
end
def authorize_index
if github_team_id
github_team_authenticate!(github_team_id)
end
end
def authorize_repo(repo)
if team_id = (repo.github_team_id || github_team_id)
github_team_authenticate!(team_id)
end
end
get "/?" do
authorize_index
@builds = Build.started.first(50)
mustache :index
end
get "/:build_id/output" do |build_id|
@build = Build.find(build_id)
authorize_repo(@build.repo)
mustache :console, :layout => false
end
get "/:repo_name" do |repo_name|
repo = find_repo(repo_name)
authorize_repo(repo)
@builds = repo.builds.started.first(50)
mustache :index
end
get "/:repo_name/:branch" do |repo_name, branch|
repo = find_repo(repo_name)
authorize_repo(repo)
@builds = repo.branch_for(branch).builds.started.first(50)
mustache :index
end
end
class NoAuth < Sinatra::Base
register Helpers
get "/boomtown" do
raise Error, "boomtown"
end
get "/site/sha" do
`cd /data/janky && git rev-parse HEAD`
end
end
end

Просмотреть файл

@ -0,0 +1,48 @@
class Init < ActiveRecord::Migration
def self.up
create_table :repositories, :force => true do |t|
t.string :name, :null => false
t.string :uri, :null => false
t.integer :room_id, :default => 376289, :null => false # Builds room
t.timestamps
end
add_index :repositories, :name, :unique => true
add_index :repositories, :uri, :unique => true
create_table :branches, :force => true do |t|
t.string :name, :null => false
t.belongs_to :repository, :null => false
t.timestamps
end
add_index :branches, [:name, :repository_id], :unique => true
create_table :commits, :force => true do |t|
t.string :sha1, :null => false
t.string :message, :null => false
t.string :author, :null => false
t.datetime :committed_at
t.belongs_to :repository, :null => false
t.timestamps
end
add_index :commits, [:sha1, :repository_id], :unique => true
create_table :builds, :force => true do |t|
t.boolean :green, :default => false
t.string :url, :null => true
t.string :compare, :null => false
t.datetime :started_at
t.datetime :completed_at
t.belongs_to :commit, :null => false
t.belongs_to :branch, :null => false
t.timestamps
end
add_index :builds, :url, :unique => true
end
def self.down
drop_table :repositories
drop_table :branches
drop_table :commits
drop_table :builds
end
end

Просмотреть файл

@ -0,0 +1,10 @@
class NonUniqueRepoUri < ActiveRecord::Migration
def self.up
remove_index :repositories, :uri
add_index :repositories, :uri
end
def self.down
add_index :repositories, :uri, :unique => true
end
end

Просмотреть файл

@ -0,0 +1,11 @@
class RepoEnabled < ActiveRecord::Migration
def self.up
add_column :repositories, :enabled, :boolean, :null => false, :default => true
add_index :repositories, :enabled
end
def self.down
remove_column :repositories, :enabled
remove_index :repositories, :enabled
end
end

Просмотреть файл

@ -0,0 +1,9 @@
class AddBuildOutputColumn < ActiveRecord::Migration
def self.up
add_column :builds, :output, :text, :null => true, :default => nil
end
def self.down
remove_column :builds, :output
end
end

Просмотреть файл

@ -0,0 +1,9 @@
class AddCommitUrlColumn < ActiveRecord::Migration
def self.up
add_column :commits, :url, :string, :null => false
end
def self.down
remove_column :commits, :url
end
end

Просмотреть файл

@ -0,0 +1,9 @@
class AddRepoHookUrl < ActiveRecord::Migration
def self.up
add_column :repositories, :hook_url, :string, :null => true
end
def self.down
remove_column :repositories, :hook_url
end
end

Просмотреть файл

@ -0,0 +1,9 @@
class AddBuildRoomId < ActiveRecord::Migration
def self.up
add_column :builds, :room_id, :integer, :null => true
end
def self.down
remove_column :builds, :room_id
end
end

Просмотреть файл

@ -0,0 +1,9 @@
class DropDefaultRoomId < ActiveRecord::Migration
def self.up
change_column :repositories, :room_id, :integer, :default => nil, :null => true
end
def self.down
change_column :repositories, :room_id, :integer, :default => 376289, :null => false
end
end

Просмотреть файл

@ -0,0 +1,9 @@
class GithubTeamId < ActiveRecord::Migration
def self.up
add_column :repositories, :github_team_id, :integer, :null => true
end
def self.down
remove_column :repositories, :github_team_id
end
end

Просмотреть файл

@ -0,0 +1,68 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your
# database schema. If you need to create the application database on another
# system, you should be using db:schema:load, not running all the migrations
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 1317384649) do
create_table "branches", :force => true do |t|
t.string "name", :null => false
t.integer "repository_id", :null => false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "branches", ["name", "repository_id"], :name => "index_branches_on_name_and_repository_id", :unique => true
create_table "builds", :force => true do |t|
t.boolean "green", :default => false
t.string "url"
t.string "compare", :null => false
t.datetime "started_at"
t.datetime "completed_at"
t.integer "commit_id", :null => false
t.integer "branch_id", :null => false
t.datetime "created_at"
t.datetime "updated_at"
t.text "output", :limit => 2147483647
t.integer "room_id"
end
add_index "builds", ["url"], :name => "index_builds_on_url", :unique => true
create_table "commits", :force => true do |t|
t.string "sha1", :null => false
t.string "message", :null => false
t.string "author", :null => false
t.datetime "committed_at"
t.integer "repository_id", :null => false
t.datetime "created_at"
t.datetime "updated_at"
t.string "url", :null => false
end
add_index "commits", ["sha1", "repository_id"], :name => "index_commits_on_sha1_and_repository_id", :unique => true
create_table "repositories", :force => true do |t|
t.string "name", :null => false
t.string "uri", :null => false
t.integer "room_id"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "enabled", :default => true, :null => false
t.string "hook_url"
t.integer "github_team_id"
end
add_index "repositories", ["enabled"], :name => "index_repositories_on_enabled"
add_index "repositories", ["name"], :name => "index_repositories_on_name", :unique => true
add_index "repositories", ["uri"], :name => "index_repositories_on_uri"
end

Двоичные данные
lib/janky/database/seed.dump.gz Normal file

Двоичный файл не отображается.

62
lib/janky/exception.rb Normal file
Просмотреть файл

@ -0,0 +1,62 @@
module Janky
module Exception
def self.setup(notifier)
@notifier = notifier
end
def self.report(exception, context={})
@notifier.report(exception, context)
end
def self.push(context)
@notifier.push(context)
end
def self.reset!
@notifier.reset!
end
def self.push_http_response(response)
push(
:response_code => response.code.inspect,
:response_body => response.body.inspect
)
end
class Middleware
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
Exception.reset!
Exception.push(
:app => "janky",
:method => request.request_method,
:user_agent => request.user_agent,
:params => (request.params.inspect rescue nil),
:session => (request.session.inspect rescue nil),
:referrer => request.referrer,
:remote_ip => request.ip,
:url => request.url
)
@app.call(env)
rescue Object => boom
Exception.report(boom)
raise
end
end
class Mock
def self.push(context)
end
def self.report(e, context={})
end
def self.reset!
end
end
end
end

67
lib/janky/github.rb Normal file
Просмотреть файл

@ -0,0 +1,67 @@
module Janky
module GitHub
def self.setup(user, password, secret, url)
@user = user
@password = password
@secret = secret
@url = url
end
class << self
attr_reader :secret
end
def self.enable_mock!
@api = Mock.new(@user, @password)
end
def self.repo_make_private(nwo)
api.make_private(nwo)
end
def self.repo_make_public(nwo)
api.make_public(nwo)
end
def self.repo_make_unauthorized(nwo)
api.make_unauthorized(nwo)
end
def self.receiver
@receiver ||= Receiver.new(@secret)
end
def self.repo_get(nwo)
response = api.repo_get(nwo)
case response.code
when "200"
Yajl.load(response.body)
when "403", "404"
nil
else
Exception.push_http_response(response)
raise Error, "Failed to get hook"
end
end
def self.hook_create(nwo)
response = api.create(nwo, @secret, @url)
if response.code == "201"
Yajl.load(response.body)["url"]
else
Exception.push_http_response(response)
raise Error, "Failed to create hook"
end
end
def self.hook_exists?(url)
api.get(url).code == "200"
end
def self.api
@api ||= API.new(@user, @password)
end
end
end

69
lib/janky/github/api.rb Normal file
Просмотреть файл

@ -0,0 +1,69 @@
module Janky
module GitHub
class API
def initialize(user, password)
@user = user
@password = password
end
def create(nwo, secret, url)
request = Net::HTTP::Post.new("/repos/#{nwo}/hooks")
payload = build_payload(url, secret)
request.body = Yajl.dump(payload)
request.basic_auth(@user, @password)
http.request(request)
end
def trigger(hook_url)
path = URI(hook_url).path
request = Net::HTTP::Post.new("#{path}/test")
request.basic_auth(@user, @password)
http.request(request)
end
def get(hook_url)
path = URI(hook_url).path
request = Net::HTTP::Get.new(path)
request.basic_auth(@user, @password)
http.request(request)
end
def repo_get(nwo)
path = "/repos/#{nwo}"
request = Net::HTTP::Get.new(path)
request.basic_auth(@user, @password)
http.request(request)
end
def build_payload(url, secret)
{ "name" => "web",
"active" => true,
"config" => {
"url" => url,
"secret" => secret,
"content_type" => "json"
}
}
end
def http
@http ||= http!
end
def http!
uri = URI("https://api.github.com")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.ca_path = "/etc/ssl/certs"
http
end
end
end
end

Просмотреть файл

@ -0,0 +1,27 @@
module Janky
module GitHub
class Commit
def initialize(sha1, url, message, author, time)
@sha1 = sha1
@url = url
@message = message
@author = author
@time = time
end
attr_reader :sha1, :url, :message, :author
def committed_at
@time
end
def to_hash
{ :id => @sha1,
:url => @url,
:message => @message,
:author => {:name => @author},
:timestamp => @time }
end
end
end
end

47
lib/janky/github/mock.rb Normal file
Просмотреть файл

@ -0,0 +1,47 @@
module Janky
module GitHub
class Mock
Response = Struct.new(:code, :body)
def initialize(user, password)
@repos = {}
end
def make_private(nwo)
@repos[nwo] = :private
end
def make_public(nwo)
@repos[nwo] = :public
end
def make_unauthorized(nwo)
@repos[nwo] = :unauthorized
end
def create(nwo, secret, url)
data = {"url" => "https://api.github.com/hooks/#{Time.now.to_f}"}
Response.new("201", Yajl.dump(data))
end
def get(url)
Response.new("200")
end
def repo_get(nwo)
repo = {
"name" => nwo.split("/").last,
"private" => (@repos[nwo] == :private),
"git_url" => "git://github.com/#{nwo}",
"ssh_url" => "git@github.com:#{nwo}"
}
if @repos[nwo] == :unauthorized
Response.new("404", Yajl.dump({}))
else
Response.new("200", Yajl.dump(repo))
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,34 @@
module Janky
module GitHub
class Payload
def self.parse(json)
parsed = PayloadParser.new(json)
new(parsed.uri, parsed.branch, parsed.head, parsed.commits, parsed.compare)
end
def initialize(uri, branch, head, commits, compare)
@uri = uri
@branch = branch
@head = head
@commits = commits
@compare = compare
end
attr_reader :uri, :branch, :head, :commits, :compare
def head_commit
@commits.detect do |commit|
commit.sha1 == @head
end
end
def to_json
{ :after => @head,
:ref => "refs/heads/#{@branch}",
:uri => @uri,
:commits => @commits,
:compare => @compare }.to_json
end
end
end
end

Просмотреть файл

@ -0,0 +1,57 @@
module Janky
module GitHub
class PayloadParser
def initialize(json)
@payload = Yajl.load(json)
end
def head
@payload["after"]
end
def compare
@payload["compare"]
end
def commits
@payload["commits"].map do |commit|
GitHub::Commit.new(
commit["id"],
commit["url"],
commit["message"],
normalize_author(commit["author"]),
commit["timestamp"]
)
end
end
def normalize_author(author)
if email = author["email"]
"#{author["name"]} <#{email}>"
else
author
end
end
def uri
if uri = @payload["uri"]
return uri
end
repository = @payload["repository"]
if repository["private"]
"git@github.com:#{URI(repository["url"]).path[1..-1]}"
else
uri = URI(repository["url"])
uri.scheme = "git"
uri.to_s
end
end
def branch
@payload["ref"].split("refs/heads/").last
end
end
end
end

Просмотреть файл

@ -0,0 +1,69 @@
module Janky
module GitHub
# Rack app handling GitHub Post-Receive [1] requests.
#
# The JSON payload is parsed into a GitHub::Payload. We then find the
# associated Repository record based on the Payload's repository git URL
# and create the associated records: Branch, Commit and Build.
#
# Finally, we trigger a new Jenkins build.
#
# [1]: http://help.github.com/post-receive-hooks/
class Receiver
def initialize(secret)
@secret = secret
end
def call(env)
dup.call!(env)
end
def call!(env)
@request = Rack::Request.new(env)
if !valid_signature?
return Rack::Response.new("Invalid signature", 403).finish
end
if !payload.head_commit
return Rack::Response.new("Ignored", 400).finish
end
result = BuildRequest.handle(
payload.uri,
payload.branch,
payload.head_commit,
payload.compare,
@request.POST["room"]
)
Rack::Response.new("OK: #{result}", 201).finish
end
def valid_signature?
digest = OpenSSL::Digest::Digest.new("sha1")
signature = @request.env["HTTP_X_HUB_SIGNATURE"].split("=").last
signature == OpenSSL::HMAC.hexdigest(digest, @secret, data)
end
def payload
@payload ||= GitHub::Payload.parse(data)
end
def data
@data ||= data!
end
def data!
if @request.content_type != "application/json"
return Rack::Response.new("Invalid Content-Type", 400).finish
end
body = ""
@request.body.each { |chunk| body << chunk }
body
end
end
end
end

17
lib/janky/helpers.rb Normal file
Просмотреть файл

@ -0,0 +1,17 @@
module Janky
module Helpers
def self.registered(app)
app.enable :raise_errors
app.disable :show_exceptions
app.helpers self
end
def find_repo(name)
unless repo = Repository.find_by_name(name)
halt(404, "Unknown repository: #{name.inspect}")
end
repo
end
end
end

117
lib/janky/hubot.rb Normal file
Просмотреть файл

@ -0,0 +1,117 @@
module Janky
# Web API taylored for Hubot's needs. Supports setting up and disabling
# repositories, querying the status of branch or a repository and triggering
# builds.
#
# The client side implementation is at
# <https://github.com/github/hubot/blob/master/scripts/ci.js>
class Hubot < Sinatra::Base
register Helpers
# Setup a new repository.
post "/setup" do
nwo = params["nwo"]
name = params["name"]
repo = Repository.setup(nwo, name)
if repo
url = "#{settings.base_url}/#{repo.name}"
[201, "Setup #{repo.name} at #{repo.uri} | #{url}"]
else
[400, "Couldn't access #{nwo}. Check the permissions."]
end
end
# Activate/deactivate auto-build for the given repository.
post "/toggle/:repo_name" do |repo_name|
repo = find_repo(repo_name)
status = repo.toggle_auto_build ? "enabled" : "disabled"
[200, "#{repo.name} is now #{status}"]
end
# Build a repository's branch.
post "/:repo_name/:branch" do |repo_name, branch_name|
repo = find_repo(repo_name)
branch = repo.branch_for(branch_name)
build = branch.current_build
room_id = params["room_id"] && Integer(params["room_id"])
if build
build.rerun(room_id)
[201, "Going ham on #{build.repo_name}/#{build.branch_name}"]
else
[404, "Unknown branch #{branch_name.inspect}. Push again"]
end
end
# Get a list of available rooms.
get "/rooms" do
Yajl.dump(Campfire.room_names)
end
# Update a repository's notification room.
put "/:repo_name" do |repo_name|
repo = find_repo(repo_name)
room = params["room"]
if room_id = Campfire.room_id(room)
repo.update_attributes!(:room_id => room_id)
[200, "Room for #{repo.name} updated to #{room}"]
else
[403, "Unknown room: #{room.inspect}"]
end
end
# Get the status of all projects.
get "/" do
content_type "text/plain"
repos = Repository.all.map do |repo|
master = repo.branch_for("master")
"%-17s %-13s %-10s %40s" % [
repo.name,
master.status,
repo.campfire_room,
repo.uri
]
end
repos.join("\n")
end
# Get the status of a repository's branch.
get "/:repo_name/:branch_name" do |repo_name, branch_name|
limit = params["limit"]
repo = find_repo(repo_name)
branch = repo.branch_for(branch_name)
builds = branch.completed_builds.limit(limit).map do |build|
{ :sha1 => build.sha1,
:repo => build.repo_name,
:branch => build.branch_name,
:green => build.green?,
:building => branch.building?,
:number => build.number,
:status => (build.green? ? "was successful" : "failed"),
:compare => build.compare,
:duration => build.duration }
end
builds.to_json
end
# Learn everything you need to know about Janky.
get "/help" do
content_type "text/plain"
<<-EOS
hubot ci build janky
hubot ci build janky/fix-everything
hubot ci setup github/janky [name]
hubot ci toggle janky
hubot ci rooms
hubot ci set room janky The Danger Room
EOS
end
end
end

111
lib/janky/job_creator.rb Normal file
Просмотреть файл

@ -0,0 +1,111 @@
module Janky
class JobCreator
def initialize(server_url, callback_url)
@server_url = server_url
@callback_url = callback_url
end
def run(name, uri, template_path)
creator.run(name, uri, template_path)
end
def creator
@creator ||= Creator.new(HTTP, @server_url, @callback_url)
end
def enable_mock!
@creator = Creator.new(Mock.new, @server_url, @callback_url)
end
class Creator
def initialize(adapter, server_url, callback_url)
@adapter = adapter
@server_url = server_url
@callback_url = callback_url
end
def run(name, uri, template_path)
template = Tilt.new(template_path.to_s)
config = template.render(Object.new, {
:name => name,
:repo => uri,
:callback_url => @callback_url
})
exception_context(config, name, uri)
if !@adapter.exists?(@server_url, name)
@adapter.run(@server_url, name, config)
true
end
end
def exception_context(config, name, uri)
Exception.push(
:server_url => @server_url.inspect,
:callback_url => @callback_url.inspect,
:adapter => @adapter.inspect,
:config => config.inspect,
:name => name.inspect,
:repo => uri.inspect
)
end
end
class Mock
def run(server_url, name, config)
name || raise(Error, "no name")
config || raise(Error, "no config")
(URI === server_url) || raise(Error, "server_url is not a URI")
true
end
def exists?(server_url, name)
false
end
end
class HTTP
def self.exists?(server_url, name)
uri = server_url
user = uri.user
pass = uri.password
http = Net::HTTP.new(uri.host, uri.port)
get = Net::HTTP::Get.new("/job/#{name}/")
get.basic_auth(user, pass) if user && pass
response = http.request(get)
case response.code
when "200"
true
when "404"
false
else
Exception.push_http_response(response)
raise "Failed to determine job existance"
end
end
def self.run(server_url, name, config)
uri = server_url
user = uri.user
pass = uri.password
http = Net::HTTP.new(uri.host, uri.port)
post = Net::HTTP::Post.new("/createItem?name=#{name}")
post.basic_auth(user, pass) if user && pass
post["Content-Type"] = "application/xml"
post.body = config
response = http.request(post)
unless response.code == "200"
Exception.push_http_response(response)
raise Error, "Failed to create job"
end
end
end
end
end

84
lib/janky/notifier.rb Normal file
Просмотреть файл

@ -0,0 +1,84 @@
module Janky
module Notifier
# Setup the notifier.
#
# notifiers - One or more notifiers implementation to notify with.
#
# Returns nothing.
def self.setup(notifiers)
@adapter = Multi.new(Array(notifiers))
end
# Called whenever a build starts.
#
# build - the Build record.
#
# Returns nothing.
def self.started(build)
adapter.started(build)
end
# Called whenever a build completes.
#
# build - the Build record.
#
# Returns nothing.
def self.completed(build)
adapter.completed(build)
end
# The implementation used to send notifications.
#
# Returns a Multi instance by default or Mock when in mock mode.
def self.adapter
@adapter ||= Multi.new(@notifiers)
end
# Enable mocking. Once enabled, notifications are stored in a
# in-memory Array exposed by the notifications method.
#
# Returns nothing.
def self.enable_mock!
@adapter = Mock.new
end
# Reset notification log. Only available when mocked. Typically called
# before each test.
#
# Returns nothing.
def self.reset!
adapter.reset!
end
# Was any notification sent out? Only available when mocked.
#
# Returns a Boolean.
def self.empty?
notifications.empty?
end
# Was a success notification sent to the given room for the given
# repo and branch?
#
# repo - the String repository name.
# branch - the String branch name.
# room - the optional String Campfire room slug.
#
# Returns a boolean.
def self.success?(repo, branch, room=nil)
adapter.success?(repo, branch, room)
end
# Same as `success?` but for failed notifications.
def self.failure?(repo, branch, room=nil)
adapter.failure?(repo, branch, room)
end
# Access the notification log. Only available when mocked.
#
# Returns an Array of notified Builds.
def self.notifications
adapter.notifications
end
end
end

Просмотреть файл

@ -0,0 +1,21 @@
module Janky
module Notifier
class Campfire
def self.completed(build)
status = build.green? ? "was successful" : "failed"
message = "Build #%s (%s) of %s/%s %s (%ss) %s" % [
build.number,
build.sha1,
build.repo_name,
build.branch_name,
status,
build.duration,
build.compare
]
::Janky::Campfire.speak(message, build.room_id)
end
end
end
end

Просмотреть файл

@ -0,0 +1,55 @@
module Janky
module Notifier
# Mock notifier implementation used in testing environments.
class Mock
def initialize
@notifications = []
end
attr_reader :notifications
def reset!
@notifications.clear
end
def started(build)
end
def completed(build)
notify(:completed, build)
end
def notify(state, build)
@notifications << [state, build]
end
def success?(repo, branch, room_name)
room_name ||= Janky::Campfire.default_room_name
builds = @notifications.select do |state, build|
state == :completed &&
build.green? &&
build.repo_name == repo &&
build.branch_name == branch &&
build.room_name == room_name
end
builds.size == 1
end
def failure?(repo, branch, room_name)
room_name ||= Janky::Campfire.default_room_name
builds = @notifications.select do |state, build|
state == :completed &&
build.red? &&
build.repo_name == repo &&
build.branch_name == branch &&
build.room_name == room_name
end
builds.size == 1
end
end
end
end

Просмотреть файл

@ -0,0 +1,22 @@
module Janky
module Notifier
# Dispatches notifications to multiple notifiers.
class Multi
def initialize(notifiers)
@notifiers = notifiers
end
def started(build)
@notifiers.each do |notifier|
notifier.started(build) if notifier.respond_to?(:started)
end
end
def completed(build)
@notifiers.each do |notifier|
notifier.completed(build) if notifier.respond_to?(:completed)
end
end
end
end
end

Просмотреть файл

@ -0,0 +1,204 @@
/*------------------------------------------------------------------------------------
@group Global Reset
------------------------------------------------------------------------------------*/
* {
padding:0;
margin:0;
}
h1, h2, h3, h4, h5, h6, p, pre, blockquote, label, ul, ol, dl, fieldset, address { margin:1em 0; }
li, dd { margin-left:5%; }
fieldset { padding: .5em; }
select option{ padding:0 5px; }
.access{ display:none; } /* For accessibility related elements */
.clear{ clear:both; height:0px; font-size:0px; line-height:0px; overflow:hidden; }
a{ outline:none; }
a img{ border:none; }
.clearfix:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
* html .clearfix {height: 1%;}
.clearfix {display:inline-block;}
.clearfix {display: block;}
/* @end */
/*----------------------------------------------------------------------------
@group Base Layout
----------------------------------------------------------------------------*/
body{
margin:0;
padding:0;
font-size:14px;
line-height:1.6;
font-family:Helvetica, Arial, sans-serif;
background:#fff;
}
#wrapper{
margin:0 auto;
width:600px;
}
.wide #wrapper{
width:1000px;
}
h2#logo{
width:600px;
margin:0 auto 25px auto;
}
h2#logo a{
display:block;
height:156px;
text-indent:-9999px;
text-decoration:none;
background:url(../images/logo.png);
}
.content{
padding:5px;
background:#ededed;
border-radius:4px;
margin-bottom: 50px;
}
.content > .inside{
border:1px solid #ddd;
background:#fff;
border-radius:3px;
}
/* @end */
/*----------------------------------------------------------------------------
@group Builds
----------------------------------------------------------------------------*/
ul.builds{
margin:0;
}
ul.builds li{
list-style-type:none;
margin:0;
padding:12px 10px;
border-bottom:1px solid #e5e5e5;
border-top:1px solid #fff;
background:-webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f2f2f2));
background:-moz-linear-gradient(top, #fdfdfd, #f2f2f2);
}
ul.builds li:first-child{
border-top:none;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
ul.builds li:last-child{
border-bottom:none;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
ul.builds li:hover{
background:-webkit-gradient(linear, left top, left bottom, from(#f5f9fb), to(#e9eef0));
background:-moz-linear-gradient(top, #f5f9fb, #e9eef0);
}
ul.builds li.building:hover{
background:-webkit-gradient(linear, left top, left bottom, from(#fdfdfd), to(#f2f2f2));
background:-moz-linear-gradient(top, #fdfdfd, #f2f2f2);
}
ul.builds a{
display:block;
text-decoration:none;
background:url(../images/disclosure-arrow.png) 100% 10px no-repeat;
}
ul.builds li:hover a{
background-position:100% -90px;
}
ul.builds .building a{
background:none;
cursor:default;
}
ul.builds .status{
float:left;
margin-top:5px;
margin-right:10px;
width:37px;
height:34px;
background:url(../images/robawt-status.gif) 0 0 no-repeat;
}
ul.builds .building .status{
background:url(../images/building-bot.gif);
}
ul.builds .janky .status{
background-position:0 -200px;
}
ul.builds h2{
margin:0;
font-size:16px;
text-shadow:0 1px #fff;
}
ul.builds .good a h2{
color:#358c00;
}
ul.builds .building a h2{
color:#e59741;
}
ul.builds .janky a h2{
color:#ae0000;
}
ul.builds p{
margin:-2px 0 0 0;
font-size:13px;
font-weight:200;
color:#666;
text-shadow:0 1px #fff;
}
ul.builds .building p{
color:#999;
}
/* @end */
/*----------------------------------------------------------------------------
@group Text Styles
----------------------------------------------------------------------------*/
pre{
margin:10px;
font-size:12px;
overflow:auto;
}
pre::-webkit-scrollbar {
height: 8px;
width: 8px;
}
pre::-webkit-scrollbar-track-piece{
margin-bottom:10px;
background-color: #e5e5e5;
border-bottom-left-radius: 4px 4px;
border-bottom-right-radius: 4px 4px;
border-top-left-radius: 4px 4px;
border-top-right-radius: 4px 4px;
}
pre::-webkit-scrollbar-thumb:vertical{
height: 25px;
background-color: #ccc;
-webkit-border-radius: 4px;
-webkit-box-shadow: 0 1px 1px rgba(255,255,255,1);
}
pre::-webkit-scrollbar-thumb:horizontal{
width: 25px;
background-color: #ccc;
-webkit-border-radius: 4px;
}
/* @end */

Двоичные данные
lib/janky/public/images/building-bot.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.2 KiB

Двоичные данные
lib/janky/public/images/disclosure-arrow.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичные данные
lib/janky/public/images/logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 27 KiB

Двоичные данные
lib/janky/public/images/robawt-status.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB

Просмотреть файл

@ -0,0 +1,3 @@
$(function(){
$('.relatize').relatize()
})

16
lib/janky/public/javascripts/jquery.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,111 @@
// jQuery Port of Rick Olson's relatize date Prototype plugin
(function($) {
$.fn.relatize = function() {
return $(this).each(function() {
if ($(this).hasClass( 'relatized' )) return
$(this).text( $.relatize(this) ).addClass( 'relatized' )
})
}
$.relatize = function(element) {
var dateStr = $(element).text()
var dateObj = new Date(dateStr)
if (isNaN(dateObj)){
// Rails outputs something like Thu Nov 12 16:00:33 -0800 2009
// IE7 can't parse this, it wants something like
// Thu Nov 12 2009 16:00:33 -0800
var regex = /(\d\d:\d\d:\d\d [+-]\d{4}) (\d{4})$/
dateObj = new Date(dateStr.replace(regex, "$2 $1"))
if (isNaN(dateObj)){
return dateStr;
}
}
return $.relatize.timeAgoInWords(dateObj)
}
// shortcut
var $r = $.relatize
$.extend($.relatize, {
shortDays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
days: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday' ],
shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
months: [ 'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November',
'December' ],
/**
* Given a formatted string, replace the necessary items and return.
* Example: Time.now().strftime("%B %d, %Y") => February 11, 2008
* @param {String} format The formatted string used to format the results
*/
strftime: function(date, format) {
var day = date.getDay(), month = date.getMonth();
var hours = date.getHours(), minutes = date.getMinutes();
var pad = function(num) {
var string = num.toString(10);
return new Array((2 - string.length) + 1).join('0') + string
};
return format.replace(/\%([aAbBcdHImMpSwyY])/g, function(part) {
switch(part.substr(1, 1)) {
case 'a': return $r.shortDays[day]; break;
case 'A': return $r.days[day]; break;
case 'b': return $r.shortMonths[month]; break;
case 'B': return $r.months[month]; break;
case 'c': return date.toString(); break;
case 'd': return pad(date.getDate()); break;
case 'H': return pad(hours); break;
case 'I': return pad((hours + 12) % 12); break;
case 'm': return pad(month + 1); break;
case 'M': return pad(minutes); break;
case 'p': return hours > 12 ? 'PM' : 'AM'; break;
case 'S': return pad(date.getSeconds()); break;
case 'w': return day; break;
case 'y': return pad(date.getFullYear() % 100); break;
case 'Y': return date.getFullYear().toString(); break;
}
})
},
timeAgoInWords: function(targetDate, includeTime) {
return $r.distanceOfTimeInWords(targetDate, new Date(), includeTime);
},
/**
* Return the distance of time in words between two Date's
* Example: '5 days ago', 'about an hour ago'
* @param {Date} fromTime The start date to use in the calculation
* @param {Date} toTime The end date to use in the calculation
* @param {Boolean} Include the time in the output
*/
distanceOfTimeInWords: function(fromTime, toTime, includeTime) {
var delta = parseInt((toTime.getTime() - fromTime.getTime()) / 1000);
if (delta < 60) {
return 'just now';
} else if (delta < 120) {
return 'about a minute ago';
} else if (delta < (45*60)) {
return (parseInt(delta / 60)).toString() + ' minutes ago';
} else if (delta < (120*60)) {
return 'about an hour ago';
} else if (delta < (24*60*60)) {
return 'about ' + (parseInt(delta / 3600)).toString() + ' hours ago';
} else if (delta < (48*60*60)) {
return '1 day ago';
} else {
var days = (parseInt(delta / 86400)).toString();
if (days > 5) {
var fmt = '%B %d, %Y'
if (includeTime) fmt += ' %I:%M %p'
return $r.strftime(fromTime, fmt);
} else {
return days + " days ago"
}
}
}
})
})(jQuery);

174
lib/janky/repository.rb Normal file
Просмотреть файл

@ -0,0 +1,174 @@
module Janky
class Repository < ActiveRecord::Base
has_many :branches
has_many :commits
has_many :builds, :through => :branches
replicate_associations :builds, :commits, :branches
default_scope(order("name"))
def self.setup(nwo, name = nil)
if nwo.nil?
raise ArgumentError, "nwo can't be nil"
end
if repo = Repository.find_by_name(nwo)
repo.setup
return repo
end
repo = GitHub.repo_get(nwo)
return if !repo
uri = repo["private"] ? repo["ssh_url"] : repo["git_url"]
name ||= repo["name"]
uri.gsub!(/\.git$/, "")
repo =
if repo = Repository.find_by_name(name)
repo.update_attributes!(:uri => uri)
repo
else
Repository.create!(:name => name, :uri => uri)
end
repo.setup
repo
end
# Find a named repository.
#
# name - The String name of the repository.
#
# Returns a Repository or nil when it doesn't exists.
def self.by_name(name)
find_by_name(name)
end
# Toggle auto-build feature of this repo. When enabled (default),
# all branches are built automatically.
#
# Returns the new flag status as a Boolean.
def toggle_auto_build
toggle(:enabled)
save!
enabled
end
# Create or retrieve the named branch.
#
# name - The branch's name as a String.
#
# Returns a Branch record.
def branch_for(name)
branches.find_or_create_by_name(name)
end
# Create or retrieve the given commit.
#
# name - The Hash representation of the Commit.
#
# Returns a Commit record.
def commit_for(commit)
commits.find_by_sha1(commit[:sha1]) ||
commits.create(commit)
end
# Jenkins host executing this repo's builds.
#
# Returns a Builder::Client.
def builder
Builder.pick_for(self)
end
# GitHub user owning this repo.
#
# Returns the user name as a String.
def github_owner
uri[/github\.com[\/:](\w+)\//] && $1
end
# Name of this repository on GitHub.
#
# Returns the name as a String.
def github_name
uri[/github\.com[\/:](\w+)\/([a-zA-Z0-9\-_]+)/] && $2
end
# Name of the Campfire room receiving build notifications.
#
# Returns the name as a String.
def campfire_room
Campfire.room_name(room_id)
end
# Ditto but returns the Fixnum room id. Defaults to the one set
# in Campfire.setup.
def room_id
read_attribute(:room_id) || Campfire.default_room_id
end
# Setups GitHub and Jenkins for build this repository.
#
# Returns nothing.
def setup
setup_job
setup_hook
end
# Create a GitHub hook for this Repository and store its URL if needed.
#
# Returns nothing.
def setup_hook
if !hook_url || !GitHub.hook_exists?(hook_url)
url = GitHub.hook_create("#{github_owner}/#{github_name}")
update_attributes!(:hook_url => url)
end
end
# Creates a job on the Jenkins server for this repository configuration
# unless one already exists. Can safely be run multiple times.
#
# Returns nothing.
def setup_job
builder.setup(job_name, uri, job_config_path)
end
# The path of the Jenkins configuration template. Try "<repo-name>.xml.erb"
# first then fallback to "default.xml.erb" under the root config directory.
#
# Returns the template path as a Pathname.
def job_config_path
custom = Janky.jobs_config_dir.join("#{name.downcase}.xml.erb")
default = Janky.jobs_config_dir.join("default.xml.erb")
if custom.readable?
custom
elsif default.readable?
default
else
raise Error, "no config.xml template for repo #{id.inspect}"
end
end
# Construct the URL pointing to this Repository's Jenkins job.
#
# Returns the String URL.
def job_url
builder.url + "job/#{job_name}"
end
# Calculate the name of the Jenkins job.
#
# Returns a String hash of this Repository name and uri.
def job_name
md5 = Digest::MD5.new
md5 << name
md5 << uri
md5 << job_config_path.read
md5 << builder.callback_url.to_s
md5.hexdigest
end
end
end

36
lib/janky/tasks.rb Normal file
Просмотреть файл

@ -0,0 +1,36 @@
require "rake"
require "rake/tasklib"
module Janky
module Tasks
extend Rake::DSL
namespace :db do
desc "Run the migration(s)"
task :migrate do
path = db_dir.join("migrate").to_s
ActiveRecord::Migration.verbose = true
ActiveRecord::Migrator.migrate(path)
Rake::Task["db:schema:dump"].invoke
end
namespace :schema do
desc "Dump the database schema into a standard Rails schema.rb file"
task :dump do
require "active_record/schema_dumper"
path = db_dir.join("schema.rb").to_s
File.open(path, "w:utf-8") do |fd|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, fd)
end
end
end
end
def self.db_dir
@db_dir ||= Pathname(__FILE__).expand_path.join("../database")
end
end
end

Просмотреть файл

@ -0,0 +1,4 @@
<p>
<a href="{{ repo_path }}">{{ repo_name }}</a>/<a href="{{ branch_path }}">{{ branch_name }}</a>/<a href="{{ commit_url }}">{{ commit_short_sha }}</a>
</p>
<pre>{{ output }}</pre>

Просмотреть файл

@ -0,0 +1,11 @@
<ul class="builds">
{{# jobs }}
<li class="{{ status }}">
<a href="{{ console_path }}">
<span class="status"></span>
<h2>{{ name }}</h2>
<p>{{{ last_built_text }}}</p>
</a>
</li>
{{/ jobs }}
</ul>

Просмотреть файл

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{title}}</title>
<link rel="stylesheet" href="/css/base.css" type="text/css" />
<script src="/javascripts/jquery.js" type="text/javascript" charset="utf-8"></script>
<script src="/javascripts/jquery.relatize.js" type="text/javascript" charset="utf-8"></script>
<script src="/javascripts/application.js" type="text/javascript" charset="utf-8"></script>
</head>
<body class="{{page_class}}">
<div id="wrapper">
<h2 id="logo"><a href="{{root}}/">Janky Hubot</a></h2>
<div class="content">
<div class="inside">
{{{yield}}}
</div>
</div>
</div>
</body>
</html>

3
lib/janky/version.rb Normal file
Просмотреть файл

@ -0,0 +1,3 @@
module Janky
VERSION = "0.1.1"
end

Просмотреть файл

@ -0,0 +1,33 @@
module Janky
module Views
class Console < Layout
def repo_name
@build.repo_name
end
def repo_path
"#{root}/#{repo_name}"
end
def branch_name
@build.branch_name
end
def branch_path
"#{repo_path}/#{branch_name}"
end
def commit_url
@build.commit_url
end
def commit_short_sha
@build.sha1
end
def output
@build.output
end
end
end
end

35
lib/janky/views/index.rb Normal file
Просмотреть файл

@ -0,0 +1,35 @@
# encoding: UTF-8
module Janky
module Views
class Index < Layout
def jobs
@builds.collect do |build|
{
:console_path => "/#{build.number}/output",
:name => "#{build.repo_name}/#{build.branch_name}",
:status => css_status_for(build),
:last_built_text => last_built_text_for(build)
}
end
end
def css_status_for(build)
if build.green?
"good"
elsif build.building?
"building"
else
"janky"
end
end
def last_built_text_for(build)
if build.building?
"Building since <span class='relatize'>#{build.started_at}</span>…"
elsif build.completed?
"Built in <span>#{build.duration}</span> seconds"
end
end
end
end
end

19
lib/janky/views/layout.rb Normal file
Просмотреть файл

@ -0,0 +1,19 @@
module Janky
module Views
class Layout < Mustache
def title
"Janky Hubot"
end
def page_class
nil
end
def root
@request.env['SCRIPT_NAME']
end
end
end
end

2
script/bootstrap Executable file
Просмотреть файл

@ -0,0 +1,2 @@
#!/bin/sh
bundle install --binstubs

6
script/server Executable file
Просмотреть файл

@ -0,0 +1,6 @@
#!/bin/sh
: ${RACK_ENV:="development"}
: ${JANKY_BASE_URL:="http://localhost:9393"}
export RACK_ENV JANKY_BASE_URL
bin/shotgun -p9393 -sthin config.ru

0
test/default.xml.erb Normal file
Просмотреть файл

271
test/janky_test.rb Normal file
Просмотреть файл

@ -0,0 +1,271 @@
require File.expand_path("../test_helper", __FILE__)
class JankyTest < Test::Unit::TestCase
def setup
Janky.setup(environment)
Janky.enable_mock!
Janky.reset!
DatabaseCleaner.clean_with(:truncation)
Janky::Campfire.rooms = {1 => "enterprise", 2 => "builds"}
Janky::Campfire.default_room_name = "builds"
hubot_setup("github/github")
end
test "green build" do
Janky::Builder.green!
gh_post_receive("github")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master")
end
test "fail build" do
Janky::Builder.red!
gh_post_receive("github")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.failure?("github", "master")
end
test "pending build" do
Janky::Builder.green!
gh_post_receive("github")
assert Janky::Notifier.empty?
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master")
end
test "builds multiple repo with the same uri" do
Janky::Builder.green!
hubot_setup("github/github", "fi")
gh_post_receive("github")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master")
assert Janky::Notifier.success?("fi", "master")
end
test "notifies room that triggered the build" do
Janky::Builder.green!
gh_post_receive("github")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master", "builds")
hubot_build("github", "master", "enterprise")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master", "enterprise")
end
test "dup commit same branch" do
Janky::Builder.green!
gh_post_receive("github", "master", "sha1")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.notifications.shift
gh_post_receive("github", "master", "sha1")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.notifications.empty?
end
test "dup commit different branch" do
Janky::Builder.green!
gh_post_receive("github", "master", "sha1")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.notifications.shift
gh_post_receive("github", "issues-dashboard", "sha1")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.notifications.empty?
end
test "dup commit currently building" do
Janky::Builder.green!
gh_post_receive("github", "master", "sha1")
Janky::Builder.start!
gh_post_receive("github", "issues-dashboard", "sha1")
Janky::Builder.complete!
assert_equal 1, Janky::Notifier.notifications.size
assert Janky::Notifier.success?("github", "master")
end
test "dup commit currently red" do
Janky::Builder.red!
gh_post_receive("github", "master", "sha1")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.notifications.shift
gh_post_receive("github", "master", "sha1")
assert Janky::Notifier.notifications.empty?
end
test "dup commit disabled repo" do
hubot_setup("github/github", "fi")
hubot_toggle("fi")
gh_post_receive("github", "master")
Janky::Builder.start!
Janky::Builder.complete!
Janky::Notifier.reset!
hubot_build("fi", "master")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("fi", "master")
end
test "web dashboard" do
assert get("/").ok?
assert get("/janky").not_found?
gh_post_receive("github")
assert get("/").ok?
assert get("/github").ok?
Janky::Builder.start!
assert get("/").ok?
Janky::Builder.complete!
assert get("/").ok?
assert get("/github").ok?
assert get("/github/master").ok?
assert get("/github/strato").ok?
assert get("#{Janky::Build.last.id}/output").ok?
end
test "hubot setup" do
Janky::GitHub.repo_make_private("github/github")
assert hubot_setup("github/github").body.
include?("git@github.com:github/github")
Janky::GitHub.repo_make_public("github/github")
assert hubot_setup("github/github").body.
include?("git://github.com/github/github")
assert_equal 1, hubot_status.body.split("\n").size
hubot_setup("github/janky")
assert_equal 2, hubot_status.body.split("\n").size
Janky::GitHub.repo_make_unauthorized("github/enterprise")
assert hubot_setup("github/enterprise").body.
include?("Couldn't access github/enterprise")
assert_equal 201, hubot_setup("janky").status
end
test "hubot toggle" do
hubot_toggle("github")
gh_post_receive("github", "master", "deadbeef")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.empty?
hubot_toggle("github")
gh_post_receive("github", "master", "cream")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master")
end
test "hubot status" do
gh_post_receive("github")
Janky::Builder.start!
Janky::Builder.complete!
status = hubot_status.body
assert status.include?("github")
assert status.include?("green")
assert status.include?("builds")
hubot_build("github", "master")
assert hubot_status.body.include?("green")
Janky::Builder.start!
assert hubot_status.body.include?("building")
hubot_setup("github/janky")
assert hubot_status.body.include?("no build")
hubot_setup("github/team")
gh_post_receive("team")
assert hubot_status.ok?
end
test "hubot status repo" do
gh_post_receive("github")
Janky::Builder.start!
Janky::Builder.complete!
hubot_build("github", "master")
Janky::Builder.start!
Janky::Builder.complete!
payload = Yajl.load(hubot_status("github", "master").body)
assert_equal 2, payload.size
end
test "hubot build" do
gh_post_receive("github", "master")
Janky::Builder.start!
Janky::Builder.complete!
assert hubot_build("github", "rails3").not_found?
end
test "hubot rooms" do
response = hubot_request("GET", "/_hubot/rooms")
rooms = Yajl.load(response.body)
assert_equal ["builds", "enterprise"], rooms
end
test "hubot set room" do
gh_post_receive("github", "master")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master", "builds")
Janky::Notifier.reset!
hubot_update_room("github", "enterprise").ok?
hubot_build("github", "master")
Janky::Builder.start!
Janky::Builder.complete!
assert Janky::Notifier.success?("github", "master", "enterprise")
end
test "hubot 404s" do
assert hubot_status("janky", "master").not_found?
assert hubot_build("janky", "master").not_found?
assert hubot_build("github", "master").not_found?
end
end

107
test/test_helper.rb Normal file
Просмотреть файл

@ -0,0 +1,107 @@
$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
require "janky"
require "test/unit"
require "database_cleaner"
class Test::Unit::TestCase
def self.test(name, &block)
define_method("test_#{name.gsub(/\s+/,'_')}".to_sym, block)
end
def environment
{ "RACK_ENV" => "test",
"JANKY_CONFIG_DIR" => File.dirname(__FILE__),
"JANKY_GITHUB_USER" => "hubot",
"JANKY_GITHUB_OAUTH_TOKEN" => "token",
"JANKY_GITHUB_HOOK_SECRET" => "secret",
"JANKY_HUBOT_USER" => "hubot",
"JANKY_HUBOT_PASSWORD" => "password",
"JANKY_CAMPFIRE_ACCOUNT" => "github",
"JANKY_CAMPFIRE_TOKEN" => "token",
"JANKY_CAMPFIRE_DEFAULT_ROOM" => "Builds"
}
end
def gh_commit(sha1 = "HEAD")
Janky::GitHub::Commit.new(
sha1,
"https://github.com/github/github/commit/#{sha1}",
":octocat:",
"sr",
Time.now
)
end
def gh_payload(repo, branch, commits)
head = commits.first
Janky::GitHub::Payload.new(
repo.uri,
branch,
head.sha1,
commits,
"http://github/compare/#{branch}...master"
)
end
def get(path)
Rack::MockRequest.new(Janky.app).get(path)
end
def gh_post_receive(repo_name, branch = "master", commit = "HEAD")
repo = Janky::Repository.find_by_name!(repo_name)
payload = gh_payload(repo, branch, [gh_commit(commit)])
digest = OpenSSL::Digest::Digest.new("sha1")
sig = OpenSSL::HMAC.hexdigest(digest, Janky::GitHub.secret, payload.to_json)
Rack::MockRequest.new(Janky.app).post("/_github",
:input => payload.to_json,
"CONTENT_TYPE" => "application/json",
"HTTP_X_HUB_SIGNATURE" => "sha1=#{sig}"
)
end
def hubot_setup(nwo, name = nil)
hubot_request("POST", "/_hubot/setup", :params => {
:nwo => nwo,
:name => name
})
end
def hubot_build(repo, branch, room_name = nil)
params =
if room_id = Janky::Campfire.room_id(room_name)
{"room_id" => room_id.to_s}
else
{}
end
hubot_request("POST", "/_hubot/#{repo}/#{branch}", :params => params)
end
def hubot_status(repo=nil, branch=nil)
if repo && branch
hubot_request("GET", "/_hubot/#{repo}/#{branch}")
else
hubot_request("GET", "/_hubot")
end
end
def hubot_request(method, path, opts={})
auth = ["#{Janky::Hubot.username}:#{Janky::Hubot.password}"].pack("m*")
env = {"HTTP_AUTHORIZATION" => "Basic #{auth}"}
Rack::MockRequest.new(Janky.app).request(method, path, env.merge(opts))
end
def hubot_toggle(repo)
hubot_request("POST", "/_hubot/toggle/#{repo}")
end
def hubot_update_room(repo, room_name)
hubot_request("PUT", "/_hubot/#{repo}", :params => {
:room => room_name
})
end
end