This commit is contained in:
Коммит
bb1b0b90b7
|
@ -0,0 +1,6 @@
|
|||
.bundle
|
||||
bin
|
||||
vendor/gems
|
||||
Gemfile.lock
|
||||
*.gem
|
||||
.rbenv-version
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
source "http://rubygems.org"
|
||||
gemspec
|
|
@ -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).
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
require "janky"
|
||||
Janky.setup(ENV)
|
||||
run Janky.app
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Двоичный файл не отображается.
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 */
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 6.2 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.4 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 27 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.6 KiB |
|
@ -0,0 +1,3 @@
|
|||
$(function(){
|
||||
$('.relatize').relatize()
|
||||
})
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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);
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
bundle install --binstubs
|
|
@ -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,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
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче