agile_notifier 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +20 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/agile_notifier.gemspec +22 -0
- data/lib/agile_notifier/ci.rb +89 -0
- data/lib/agile_notifier/composer.rb +91 -0
- data/lib/agile_notifier/configuration.rb +82 -0
- data/lib/agile_notifier/git.rb +38 -0
- data/lib/agile_notifier/github.rb +57 -0
- data/lib/agile_notifier/its.rb +19 -0
- data/lib/agile_notifier/jenkins.rb +99 -0
- data/lib/agile_notifier/jira.rb +7 -0
- data/lib/agile_notifier/judger.rb +30 -0
- data/lib/agile_notifier/operating_system.rb +37 -0
- data/lib/agile_notifier/player.rb +20 -0
- data/lib/agile_notifier/response_helper.rb +41 -0
- data/lib/agile_notifier/scm.rb +40 -0
- data/lib/agile_notifier/servable.rb +55 -0
- data/lib/agile_notifier/trackable.rb +15 -0
- data/lib/agile_notifier/tts.rb +112 -0
- data/lib/agile_notifier.rb +5 -0
- data/test/ext/module.rb +22 -0
- data/test/helper.rb +6 -0
- data/test/test_operating_system.rb +16 -0
- data/test/test_response_helper.rb +17 -0
- data/test/test_servable.rb +15 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e8d6da02fafc819c407ab3abf1b53f40ab5c7fcd
|
4
|
+
data.tar.gz: 8248b6c72b97d94581e2142d174fe37fc65b7769
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 401c943e96a5b85e53e95b615bd5128c996f7d026c99561f17e05948cca16baf3af427b48da755d3b94f6bff177ee53c720e5b4cd63efc43af8a0ba7b49f3c1b
|
7
|
+
data.tar.gz: f468cb62a61bc49b88f47f9d46e22540c929732e1fb0c7a0de4cd802bc1282bc128a251c944f4dd1daf8f0620bc905db725c36edb781ea2cbaefeb11bd1aa6f2
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
httparty (0.11.0)
|
5
|
+
multi_json (~> 1.0)
|
6
|
+
multi_xml (>= 0.5.2)
|
7
|
+
json (1.8.0)
|
8
|
+
metaclass (0.0.1)
|
9
|
+
mocha (0.14.0)
|
10
|
+
metaclass (~> 0.0.1)
|
11
|
+
multi_json (1.8.0)
|
12
|
+
multi_xml (0.5.5)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
httparty
|
19
|
+
json
|
20
|
+
mocha
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
AgileNotifier
|
2
|
+
=============
|
3
|
+
|
4
|
+
Agile Notifier - an easy way of monitoring Agile SW Engineering by auralization change. It supports most of the popular tools including **CI** (Continuous Integration), **SCM** (Source Control Management), and **ITS** (Issue Tracking System). Of course you can integrate more tools as you want. And the simple beautiful DSL syntax gives you a rocket start.
|
5
|
+
|
6
|
+
In a modern agile team, members would like to be notified immediately once their Continuous Integration job fails (but building process usually takes quite a while). That's the initial idea of this tool.
|
7
|
+
|
8
|
+
The joy of this tool is that, whenever a build fails, it can blame whoever submitted the commit (or praises the committer who fixed it), in whatever language you want, and even the sentenses can be randomly chosen each time :)
|
9
|
+
|
10
|
+
Have fun with it!
|
11
|
+
|
12
|
+
## Examples of Usage:
|
13
|
+
```ruby
|
14
|
+
AgileNotifier::Configuration.set do
|
15
|
+
ci_url 'http://x.x.x.x:8080'
|
16
|
+
ci_job 'your-project-continuous-build'
|
17
|
+
ci_get 'Jenkins'
|
18
|
+
|
19
|
+
scm_url 'https://github.xyzcompany.com'
|
20
|
+
scm_repo user: 'your_user_name', repo: 'your_repository_name'
|
21
|
+
scm_get 'Github', enterprise: true
|
22
|
+
|
23
|
+
# for non-enterprise version
|
24
|
+
# scm_url 'https://api.github.com'
|
25
|
+
# scm_repo user: 'your_user_name', repo: 'your_repository_name'
|
26
|
+
# scm_get 'Github'
|
27
|
+
|
28
|
+
speak 'en'
|
29
|
+
play 'Boing' # Mac OSX TTS voice name(optional field), unnecessary for other OS
|
30
|
+
|
31
|
+
alert_on_fail
|
32
|
+
alert_on_fix
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
## Notes:
|
37
|
+
* TTS (Text To Speech) on Linux used here has two dependencies:
|
38
|
+
|
39
|
+
* TTS service benefits from online MARY TTS Web Client: http://mary.dfki.de:59125/ While it has limited languages support, please check before use.
|
40
|
+
* play command comes from package sox which is not pre-installed, you have to install manually beforehand by:
|
41
|
+
* ```
|
42
|
+
sudo apt-get install sox libsox-fmt-all
|
43
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), *%w[lib])))
|
2
|
+
require 'agile_notifier'
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
desc 'Run Unit Test'
|
8
|
+
Rake::TestTask.new do |test|
|
9
|
+
test.libs << 'test'
|
10
|
+
test.test_files = FileList['test/test*.rb']
|
11
|
+
test.verbose = true
|
12
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
$: << File.expand_path('../lib', __FILE__)
|
4
|
+
require "agile_notifier"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'agile_notifier'
|
8
|
+
s.version = AgileNotifier::VERSION
|
9
|
+
s.license = 'MIT'
|
10
|
+
s.date = '2013-05-22'
|
11
|
+
s.summary = %q{agile_notifier alerts you via making wonderful noises}
|
12
|
+
s.description = %q{agile_notifier alerts you via making wonderful noises, make software development more fun}
|
13
|
+
s.authors = ['Jing Li']
|
14
|
+
s.email = ['thyrlian@gmail.com']
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.homepage = 'https://github.com/thyrlian/AgileNotifier'
|
18
|
+
|
19
|
+
s.add_runtime_dependency('json', '~> 1.8.0')
|
20
|
+
s.add_runtime_dependency('httparty', '~> 0.11.0')
|
21
|
+
s.add_development_dependency('mocha', '~> 0.14.0')
|
22
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require_relative 'servable'
|
2
|
+
require_relative 'trackable'
|
3
|
+
|
4
|
+
module AgileNotifier
|
5
|
+
class CI
|
6
|
+
include Servable
|
7
|
+
alias_method :original_is_available?, :is_available?
|
8
|
+
|
9
|
+
attr_accessor :jobs
|
10
|
+
|
11
|
+
def initialize(url, *jobs)
|
12
|
+
@url = url
|
13
|
+
jobs.empty? ? @jobs = get_all_jobs : @jobs = jobs
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_available?
|
17
|
+
original_is_available?(@url)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_all_jobs
|
21
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
22
|
+
end
|
23
|
+
|
24
|
+
def job
|
25
|
+
if @jobs.size == 1
|
26
|
+
return @jobs.first
|
27
|
+
else
|
28
|
+
raise('There are more than one job, please use method [jobs] instead of [job]')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Job
|
33
|
+
attr_accessor :name, :url
|
34
|
+
attr_reader :last_build, :penultimate_build
|
35
|
+
|
36
|
+
def initialize(name, url)
|
37
|
+
@name = name
|
38
|
+
@url = url
|
39
|
+
@last_build = get_last_build
|
40
|
+
@penultimate_build = get_penultimate_build
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_last_build
|
44
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_penultimate_build
|
48
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_last_build
|
52
|
+
@last_build = get_last_build
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_penultimate_build
|
56
|
+
@penultimate_build = get_penultimate_build
|
57
|
+
end
|
58
|
+
|
59
|
+
class Build
|
60
|
+
include Trackable
|
61
|
+
|
62
|
+
attr_accessor :number, :url, :result, :revision
|
63
|
+
|
64
|
+
def initialize(number, url)
|
65
|
+
@number = number
|
66
|
+
@url = url
|
67
|
+
@result = get_result
|
68
|
+
@revision = get_revision
|
69
|
+
end
|
70
|
+
|
71
|
+
def get_result
|
72
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_revision
|
76
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_previous_build
|
80
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_previous_result
|
84
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module AgileNotifier
|
4
|
+
class Composer
|
5
|
+
SENTENCES_BLAME = {
|
6
|
+
de: [
|
7
|
+
"%{committer_name} hat den Build kaputt gemacht.",
|
8
|
+
"Schießt %{committer_name} mit der Nerf Gun ab!",
|
9
|
+
"%{committer_name} hat Scheiße gebaut.",
|
10
|
+
"%Hilfe! Hilfe! %{committer_name} versucht mich zu töten!"
|
11
|
+
],
|
12
|
+
en: [
|
13
|
+
"%{committer_name} has broken the build.",
|
14
|
+
"%{committer_name} fucked up the build.",
|
15
|
+
"What the fucking code has %{committer_name} pushed!"
|
16
|
+
],
|
17
|
+
es: [
|
18
|
+
"%{committer_name} ha destruido la compilacion",
|
19
|
+
"que clase de codigo esta escribiendo %{committer_name} ? ",
|
20
|
+
"tal vez es mejor que %{committer_name} se dedique a otra cosa..."
|
21
|
+
],
|
22
|
+
zh: [
|
23
|
+
"%{committer_name}在搞毛啊, 构建失败了!",
|
24
|
+
"%{committer_name}提交的什么烂代码啊?",
|
25
|
+
"请注意, %{committer_name}在搞破坏."
|
26
|
+
]
|
27
|
+
}
|
28
|
+
|
29
|
+
SENTENCES_PRAISE = {
|
30
|
+
de: [
|
31
|
+
"%{committer_name} hat den Build gefixt!",
|
32
|
+
"%{committer_name} ist ein Genie!",
|
33
|
+
"%{committer_name} hat die Welt gerettet!"
|
34
|
+
],
|
35
|
+
en: [
|
36
|
+
"%{committer_name} has fixed the build.",
|
37
|
+
"%{committer_name} is super brilliant!",
|
38
|
+
"%{committer_name} saved the world.",
|
39
|
+
"%{committer_name} roundhouse kicked chuck norris' butt"
|
40
|
+
],
|
41
|
+
es: [
|
42
|
+
"%{committer_name} ha reparado la compilacion",
|
43
|
+
"%{committer_name} es un genio!",
|
44
|
+
"%{committer_name} es el mejor programador de la historia!"
|
45
|
+
],
|
46
|
+
zh: [
|
47
|
+
"%{committer_name}很厉害啊, 修复了构建.",
|
48
|
+
"%{committer_name}是个好同志, 该涨工资了.",
|
49
|
+
"%{committer_name}是当代活雷锋啊!"
|
50
|
+
]
|
51
|
+
}
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def blame_committer_of_a_commit(args)
|
55
|
+
committer_name = get_committer_name_of_a_commit(args)
|
56
|
+
blame_committer(committer_name, args[:language])
|
57
|
+
end
|
58
|
+
|
59
|
+
def praise_committer_of_a_commit(args)
|
60
|
+
committer_name = get_committer_name_of_a_commit(args)
|
61
|
+
praise_committer(committer_name, args[:language])
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_committer_name_of_a_commit(args)
|
65
|
+
repo = args[:repo]
|
66
|
+
revision = args[:revision] || args[:build].revision
|
67
|
+
repo.get_committer_name_of_a_commit(revision)
|
68
|
+
end
|
69
|
+
|
70
|
+
def blame_committer(committer_name, language)
|
71
|
+
mention_committer(committer_name, SENTENCES_BLAME[language])
|
72
|
+
end
|
73
|
+
|
74
|
+
def praise_committer(committer_name, language)
|
75
|
+
mention_committer(committer_name, SENTENCES_PRAISE[language])
|
76
|
+
end
|
77
|
+
|
78
|
+
def mention_committer(committer_name, sentences)
|
79
|
+
sentence = random_picker(sentences)
|
80
|
+
sentence.gsub(/%\{committer_name\}/, committer_name)
|
81
|
+
end
|
82
|
+
|
83
|
+
def random_picker(list)
|
84
|
+
random_number = rand(list.size)
|
85
|
+
list[random_number]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private_class_method :get_committer_name_of_a_commit, :blame_committer, :praise_committer, :mention_committer, :random_picker
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative '../agile_notifier'
|
2
|
+
|
3
|
+
module AgileNotifier
|
4
|
+
class Configuration
|
5
|
+
def initialize(&blk)
|
6
|
+
@current_module = Object.const_get(self.class.to_s.split('::').first)
|
7
|
+
instance_eval(&blk)
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def set(&blk)
|
12
|
+
new(&blk)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def ci_url(url)
|
17
|
+
@ci_url = url
|
18
|
+
end
|
19
|
+
|
20
|
+
def ci_job(job)
|
21
|
+
@ci_jobs ||= []
|
22
|
+
@ci_jobs.push(job)
|
23
|
+
end
|
24
|
+
|
25
|
+
def ci_get(ci_type)
|
26
|
+
@ci = @current_module.const_get(ci_type).new(@ci_url, *@ci_jobs)
|
27
|
+
end
|
28
|
+
|
29
|
+
def scm_url(url)
|
30
|
+
@scm_url = url
|
31
|
+
end
|
32
|
+
|
33
|
+
def scm_repo(repo)
|
34
|
+
@scm_repos ||= []
|
35
|
+
@scm_repos.push(repo)
|
36
|
+
end
|
37
|
+
|
38
|
+
def scm_get(scm_type, args = {})
|
39
|
+
enterprise = args.fetch(:enterprise, false)
|
40
|
+
if enterprise
|
41
|
+
@scm = @current_module.const_get(scm_type).new_enterprise_version(@scm_url)
|
42
|
+
else
|
43
|
+
@scm = @current_module.const_get(scm_type).new(@scm_url)
|
44
|
+
end
|
45
|
+
@scm_repos.each do |repo|
|
46
|
+
@scm.add_repository(repo)
|
47
|
+
end
|
48
|
+
return @scm
|
49
|
+
end
|
50
|
+
|
51
|
+
def speak(language)
|
52
|
+
@language = language.to_s.downcase.intern
|
53
|
+
end
|
54
|
+
|
55
|
+
def play(voice)
|
56
|
+
@voice = voice
|
57
|
+
end
|
58
|
+
|
59
|
+
def alert_on_fail
|
60
|
+
alert(:blame, :fail)
|
61
|
+
end
|
62
|
+
|
63
|
+
def alert_on_fix
|
64
|
+
alert(:praise, :fix)
|
65
|
+
end
|
66
|
+
|
67
|
+
def alert(composer_type, judger_type)
|
68
|
+
args = Hash.new
|
69
|
+
args[:language] = @language
|
70
|
+
args[:voice] = @voice if @voice
|
71
|
+
composer_type = composer_type.to_s.downcase
|
72
|
+
judger_type = judger_type.to_s.downcase
|
73
|
+
composer_method = "#{composer_type}_committer_of_a_commit".intern
|
74
|
+
judger_method = "on_#{judger_type}".intern
|
75
|
+
text = Composer.send(composer_method, repo: @scm.repository, revision: @ci.job.last_build.revision, language: @language)
|
76
|
+
Judger.send(judger_method, @ci.job.last_build, text, args)
|
77
|
+
end
|
78
|
+
|
79
|
+
private_class_method :new
|
80
|
+
private :alert
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'scm'
|
2
|
+
|
3
|
+
module AgileNotifier
|
4
|
+
class Git < SCM
|
5
|
+
attr_accessor :repo
|
6
|
+
|
7
|
+
def initialize(dir)
|
8
|
+
@repo = Repository.new(dir)
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def combine_commands(*commands)
|
13
|
+
separator = ' && '
|
14
|
+
combined_commands = commands.inject('') do |cmds, cmd|
|
15
|
+
cmds += "#{cmd}#{separator}"
|
16
|
+
end
|
17
|
+
combined_commands.gsub!(/#{separator}$/, '')
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_command(command)
|
21
|
+
`#{command}`
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Repository < SCM::Repository
|
26
|
+
def initialize(repo)
|
27
|
+
@repo = repo
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_committer_of_a_commit(revision)
|
31
|
+
go_to_repo = "cd #{@repo}"
|
32
|
+
show_author_name = "git show #{revision} --pretty=format:%an | head -1"
|
33
|
+
cmd = Git.combine_commands(go_to_repo, show_author_name)
|
34
|
+
Git.run_command(cmd).chomp
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'httparty'
|
3
|
+
require_relative 'scm'
|
4
|
+
require_relative 'response_helper'
|
5
|
+
|
6
|
+
module AgileNotifier
|
7
|
+
class Github < SCM
|
8
|
+
extend ResponseHelper
|
9
|
+
|
10
|
+
ENTERPRISE_API = '/api/v3'
|
11
|
+
USERAGENT = 'AgileNotifier'
|
12
|
+
|
13
|
+
def initialize(url)
|
14
|
+
super
|
15
|
+
if url.include?(ENTERPRISE_API)
|
16
|
+
status_url = url + '/zen'
|
17
|
+
begin
|
18
|
+
status = HTTParty.get(status_url).code
|
19
|
+
availability = ( status == 200 )
|
20
|
+
rescue => e
|
21
|
+
puts e.message
|
22
|
+
availability = false
|
23
|
+
end
|
24
|
+
else
|
25
|
+
status_url = url.gsub(/:\/\/api\./, '://status.') + '/api/status.json'
|
26
|
+
status = self.class.get_value('status', status_url)
|
27
|
+
availability = ( status == 'good' )
|
28
|
+
end
|
29
|
+
raise('Github is not available.') unless availability
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def new_enterprise_version(url)
|
34
|
+
new(url + ENTERPRISE_API)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_value(key, url)
|
38
|
+
get_value_of_key(key, url, :headers => {'User-Agent' => USERAGENT})
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_repository(args)
|
43
|
+
user = args[:user]
|
44
|
+
repo = args[:repo]
|
45
|
+
repository = Repository.new(user: user, repo: repo, url: @url)
|
46
|
+
@repositories.push(repository)
|
47
|
+
end
|
48
|
+
|
49
|
+
class Repository < SCM::Repository
|
50
|
+
def get_committer_name_of_a_commit(revision)
|
51
|
+
url = @url + "/repos/#{@user}/#{@repo}/git/commits/#{revision}"
|
52
|
+
committer = Github.get_value('committer', url)
|
53
|
+
committer['name']
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require 'httparty'
|
4
|
+
require_relative 'ci'
|
5
|
+
require_relative 'response_helper'
|
6
|
+
|
7
|
+
module AgileNotifier
|
8
|
+
class Jenkins < CI
|
9
|
+
extend ResponseHelper
|
10
|
+
|
11
|
+
JSON_API = '/api/json'
|
12
|
+
|
13
|
+
def self.get_value(key, url)
|
14
|
+
get_value_of_key(key, url.gsub(/\/$/, '') + JSON_API)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(url, *names)
|
18
|
+
@url = url
|
19
|
+
if names.empty?
|
20
|
+
@jobs = get_all_jobs
|
21
|
+
else
|
22
|
+
@jobs = []
|
23
|
+
names.each do |name|
|
24
|
+
job_url = URI.encode("#{@url}/job/#{name}/")
|
25
|
+
@jobs.push(Job.new(name, job_url))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_all_jobs
|
31
|
+
jobs = self.class.get_value('jobs', @url)
|
32
|
+
if jobs.nil?
|
33
|
+
return nil
|
34
|
+
else
|
35
|
+
jobs.inject([]) do |all_jobs, job|
|
36
|
+
all_jobs.push(Job.new(job['name'], job['url']))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Job < CI::Job
|
42
|
+
def get_last_build
|
43
|
+
last_build = Jenkins.get_value('lastBuild', @url)
|
44
|
+
last_build.nil? ? nil : Build.new(last_build['number'], last_build['url'])
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_penultimate_build
|
48
|
+
last_build.get_previous_build
|
49
|
+
end
|
50
|
+
|
51
|
+
class Build < CI::Job::Build
|
52
|
+
include Servable
|
53
|
+
|
54
|
+
def get_result
|
55
|
+
result = Jenkins.get_value('result', @url)
|
56
|
+
result.nil? ? nil : result
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_revision
|
60
|
+
revision = Jenkins.get_value('lastBuiltRevision', @url)
|
61
|
+
revision.nil? ? nil : revision['SHA1']
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_previous_build
|
65
|
+
previous_number = @number - 1
|
66
|
+
while previous_number > 0
|
67
|
+
previous_url = @url.gsub(/\/#{@number}\//, "/#{previous_number}/")
|
68
|
+
if is_available?(previous_url)
|
69
|
+
return Build.new(previous_number, previous_url)
|
70
|
+
else
|
71
|
+
previous_number -= 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
return nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_previous_result
|
78
|
+
get_previous_build.get_result
|
79
|
+
end
|
80
|
+
|
81
|
+
def passed?
|
82
|
+
@result == 'SUCCESS'
|
83
|
+
end
|
84
|
+
|
85
|
+
def failed?
|
86
|
+
@result == 'FAILURE'
|
87
|
+
end
|
88
|
+
|
89
|
+
def fixed?
|
90
|
+
if get_previous_result == 'FAILURE'
|
91
|
+
return passed?
|
92
|
+
else
|
93
|
+
return nil # if previous result is SUCCESS, doesn't make sense, then return nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'tts'
|
2
|
+
|
3
|
+
module AgileNotifier
|
4
|
+
class Judger
|
5
|
+
class << self
|
6
|
+
def on_fail(trackable, text, args)
|
7
|
+
on_condition(trackable.failed?, text, args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_pass(trackable, text, args)
|
11
|
+
on_condition(trackable.passed?, text, args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_fix(trackable, text, args)
|
15
|
+
on_condition(trackable.fixed?, text, args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_condition(condition, text, args)
|
19
|
+
if condition
|
20
|
+
TTS.speak(text, args)
|
21
|
+
true
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private_class_method :on_condition
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module AgileNotifier
|
2
|
+
class OperatingSystem
|
3
|
+
TYPE = {
|
4
|
+
lin: 'linux',
|
5
|
+
mac: 'osx',
|
6
|
+
win: 'windows',
|
7
|
+
unknown: 'UNKNOWN'
|
8
|
+
}
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def is_linux?
|
12
|
+
match_os(/linux/)
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_mac?
|
16
|
+
match_os(/darwin/)
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_windows?
|
20
|
+
match_os(/mswin|mingw|cygwin|bccwin|wince|emx/)
|
21
|
+
end
|
22
|
+
|
23
|
+
def match_os(regex)
|
24
|
+
!!RUBY_PLATFORM.match(regex)
|
25
|
+
end
|
26
|
+
|
27
|
+
def what
|
28
|
+
return TYPE[:lin] if is_linux?
|
29
|
+
return TYPE[:mac] if is_mac?
|
30
|
+
return TYPE[:win] if is_windows?
|
31
|
+
return TYPE[:unknown]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private_class_method :new, :match_os
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AgileNotifier
|
2
|
+
class Player
|
3
|
+
class << self
|
4
|
+
def play_on_linux(file)
|
5
|
+
# play command comes from sox package, not pre-installed
|
6
|
+
system("play #{file}")
|
7
|
+
end
|
8
|
+
|
9
|
+
def play_on_osx(file)
|
10
|
+
system("afplay #{file}")
|
11
|
+
end
|
12
|
+
|
13
|
+
def play_on_windows(file)
|
14
|
+
raise(NotImplementedError, "Method [#{__method__}] is empty, please implement", caller)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private_class_method :new
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
module AgileNotifier
|
5
|
+
class ResponseError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
module ResponseHelper
|
9
|
+
def get_value_of_key(key, *args)
|
10
|
+
pm = PrivateMethods.new
|
11
|
+
pm.get_value_of_key_from_json(key, pm.get_json_response(*args))
|
12
|
+
end
|
13
|
+
|
14
|
+
class PrivateMethods
|
15
|
+
def get_json_response(*args)
|
16
|
+
response = HTTParty.get(*args)
|
17
|
+
if response.code == 200
|
18
|
+
return JSON.parse(response.body)
|
19
|
+
else
|
20
|
+
raise(ResponseError, "HTTP Status Code: #{response.code} - #{response.parsed_response}", caller)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_value_of_key_from_json(key, collection) # only returns the first match if duplicated keys exist
|
25
|
+
if collection.respond_to?(:each_pair) # behaves like Hash
|
26
|
+
return collection[key] if collection.has_key?(key)
|
27
|
+
collection.keys.each do |k|
|
28
|
+
value = send(__method__, key, collection[k]) # recursive call
|
29
|
+
return value if !value.nil?
|
30
|
+
end
|
31
|
+
elsif collection.respond_to?(:each_index) # behaves like Array
|
32
|
+
collection.each do |x|
|
33
|
+
value = send(__method__, key, x) # recursive call
|
34
|
+
return value if !value.nil?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AgileNotifier
|
2
|
+
class SCM
|
3
|
+
attr_accessor :url, :repositories
|
4
|
+
|
5
|
+
def initialize(url)
|
6
|
+
@url = url
|
7
|
+
@repositories = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_repository(repository)
|
11
|
+
@repositories.push(repository)
|
12
|
+
end
|
13
|
+
|
14
|
+
def repository
|
15
|
+
if @repositories.size == 1
|
16
|
+
return @repositories.first
|
17
|
+
else
|
18
|
+
raise('There are more than one repository, please use method [repositories] instead of [repository]')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Repository
|
23
|
+
attr_accessor :user, :repo, :url
|
24
|
+
|
25
|
+
def initialize(args)
|
26
|
+
@user = args[:user]
|
27
|
+
@repo = args[:repo]
|
28
|
+
@url = args[:url]
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_commit(revision)
|
32
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_committer_of_a_commit(revision)
|
36
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module AgileNotifier
|
4
|
+
module Servable
|
5
|
+
class PrivateMethods
|
6
|
+
def is_https_request?(url)
|
7
|
+
regex = /https:\/\//
|
8
|
+
regex.match(url) ? true : false
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_http_server_available?(url)
|
12
|
+
uri = URI(url)
|
13
|
+
begin
|
14
|
+
res = Net::HTTP.get_response(uri)
|
15
|
+
rescue Errno::ECONNREFUSED # server shutdown
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
return is_response_ok?(res)
|
19
|
+
end
|
20
|
+
|
21
|
+
def is_https_server_available?(url)
|
22
|
+
uri = URI(url)
|
23
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
24
|
+
http.use_ssl = true
|
25
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
26
|
+
begin
|
27
|
+
res = http.request(req)
|
28
|
+
rescue
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
return is_response_ok?(res)
|
32
|
+
end
|
33
|
+
|
34
|
+
def is_response_ok?(res)
|
35
|
+
status_code = res.code.to_i
|
36
|
+
if status_code == 200
|
37
|
+
return true
|
38
|
+
else # e.g. 503 (Service Unavailable)
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def is_available?(url)
|
45
|
+
pm = PrivateMethods.new
|
46
|
+
pm.instance_eval do
|
47
|
+
if is_https_request?(url)
|
48
|
+
return is_https_server_available?(url)
|
49
|
+
else
|
50
|
+
return is_http_server_available?(url)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module AgileNotifier
|
2
|
+
module Trackable
|
3
|
+
def failed?
|
4
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
5
|
+
end
|
6
|
+
|
7
|
+
def passed?
|
8
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
9
|
+
end
|
10
|
+
|
11
|
+
def fixed?
|
12
|
+
raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'open-uri'
|
3
|
+
require_relative 'operating_system'
|
4
|
+
require_relative 'player'
|
5
|
+
|
6
|
+
module AgileNotifier
|
7
|
+
class TTS
|
8
|
+
class << self
|
9
|
+
def speak(text, args)
|
10
|
+
os_type = get_os_type
|
11
|
+
service = "speak_on_#{os_type}"
|
12
|
+
language = args[:language]
|
13
|
+
voice = args[:voice]
|
14
|
+
Service.send(service.intern, text, language, voice)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_os_type
|
18
|
+
OperatingSystem.what
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Service
|
23
|
+
@@temp_file = File.dirname(__FILE__) + '/temp_tts'
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def download_file(url, file = @@temp_file)
|
27
|
+
open(url) do |remote_file|
|
28
|
+
data = remote_file.read
|
29
|
+
File.open(file, 'wb') do |local_file|
|
30
|
+
local_file.write(data)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove_file(file = @@temp_file)
|
36
|
+
File.delete(file)
|
37
|
+
end
|
38
|
+
|
39
|
+
def play(url)
|
40
|
+
download_file(url)
|
41
|
+
os_type = get_os_type
|
42
|
+
player = "play_on_#{os_type}"
|
43
|
+
Player.send(player.intern, @@temp_file)
|
44
|
+
remove_file
|
45
|
+
end
|
46
|
+
|
47
|
+
def replace_punctuation_by_space(text)
|
48
|
+
text.gsub(/[[:punct:]](\s+)*/, ' ').gsub(/\s$/, '')
|
49
|
+
end
|
50
|
+
|
51
|
+
def encode_www_form(text)
|
52
|
+
URI.encode_www_form('' => text)[1..-1]
|
53
|
+
end
|
54
|
+
|
55
|
+
def encode_www_form_and_remove_punctuation(text)
|
56
|
+
encode_www_form(replace_punctuation_by_space(text))
|
57
|
+
end
|
58
|
+
|
59
|
+
def speak_on_linux(text, language, voice = nil)
|
60
|
+
# TODO: should check service availability first, if not raise exception
|
61
|
+
mary_tts(text, language)
|
62
|
+
end
|
63
|
+
|
64
|
+
def speak_on_osx(text, language, voice)
|
65
|
+
if voice
|
66
|
+
unless osx_speech(voice, text)
|
67
|
+
raise("No available voice found, please check if you have downloaded voice [#{voice}] in System Preferences -> Speech")
|
68
|
+
end
|
69
|
+
else
|
70
|
+
list_of_available_voices = `say -v '?'`.split("\n")
|
71
|
+
voices = list_of_available_voices.inject({}) do |collection, record|
|
72
|
+
matched_results = record.match(/^(.*[^\s])\s+([a-z]{2})_[A-Z]{2}\s+/)
|
73
|
+
available_language = matched_results[2].downcase.intern
|
74
|
+
available_voice = matched_results[1]
|
75
|
+
if collection.has_key?(available_language)
|
76
|
+
collection[available_language] << available_voice
|
77
|
+
else
|
78
|
+
collection[available_language] = [available_voice]
|
79
|
+
end
|
80
|
+
collection
|
81
|
+
end
|
82
|
+
osx_speech(voices[language].first, text)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def speak_on_windows(text, language, voice = nil)
|
87
|
+
raise(NotImplementedError, "Method [#{__method__}] is empty, please implement", caller)
|
88
|
+
end
|
89
|
+
|
90
|
+
def osx_speech(voice, text)
|
91
|
+
system("say -v #{voice} #{text} > /dev/null 2>&1")
|
92
|
+
end
|
93
|
+
|
94
|
+
def tts_api(text)
|
95
|
+
url = 'http://tts-api.com/tts.mp3?q='
|
96
|
+
play(url + encode_www_form_and_remove_punctuation(text))
|
97
|
+
end
|
98
|
+
|
99
|
+
def mary_tts(text, language)
|
100
|
+
url = 'http://mary.dfki.de:59125/'
|
101
|
+
text = encode_www_form_and_remove_punctuation(text)
|
102
|
+
audio_format = 'WAVE_FILE'
|
103
|
+
play(url + "process?INPUT_TEXT=#{text}&INPUT_TYPE=TEXT&OUTPUT_TYPE=AUDIO&LOCALE=#{language}&AUDIO=#{audio_format}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private_class_method :download_file, :remove_file, :play, :replace_punctuation_by_space, :encode_www_form, :encode_www_form_and_remove_punctuation, :osx_speech
|
108
|
+
end
|
109
|
+
|
110
|
+
private_class_method :new, :get_os_type
|
111
|
+
end
|
112
|
+
end
|
data/test/ext/module.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class Module
|
2
|
+
def backup_const(name)
|
3
|
+
redef_const(:TEMP_BACKUP, const_get(name))
|
4
|
+
end
|
5
|
+
|
6
|
+
def restore_const(name)
|
7
|
+
redef_const(name, const_get(:TEMP_BACKUP))
|
8
|
+
remove_const(:TEMP_BACKUP)
|
9
|
+
end
|
10
|
+
|
11
|
+
def redef_const(name, value)
|
12
|
+
remove_const(name) if const_defined?(name)
|
13
|
+
const_set(name, value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def stub_const_and_test(const_name, const_value, &test_blk)
|
17
|
+
backup_const(const_name)
|
18
|
+
redef_const(const_name, const_value)
|
19
|
+
test_blk.call
|
20
|
+
restore_const(const_name)
|
21
|
+
end
|
22
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
require_relative 'ext/module'
|
3
|
+
|
4
|
+
class TestOperatingSystem < Test::Unit::TestCase
|
5
|
+
def test_is_mac?
|
6
|
+
Object.stub_const_and_test(:RUBY_PLATFORM, 'x86_64-darwin11.2.0') { assert_equal(true, OperatingSystem.is_mac?) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_is_linux?
|
10
|
+
Object.stub_const_and_test(:RUBY_PLATFORM, 'x86_64-linux') { assert_equal(true, OperatingSystem.is_linux?) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_is_windows?
|
14
|
+
Object.stub_const_and_test(:RUBY_PLATFORM, 'i386-mingw32') { assert_equal(true, OperatingSystem.is_windows?) }
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class TestResponseHelper < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@pm = ResponseHelper::PrivateMethods.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_get_value_of_key_from_json
|
9
|
+
json = {'actions' => [{}, {}, {'buildsByBranchName' => {'origin/master' => {'buildNumber' => 3, 'revision' => {'SHA1' => '08a2be82ba83c1e89e01f698a30203fb0284aa33', 'branch' => [{'SHA1' => '4eac3c2c5b8f2daddeaa37926e7c1f55cd756f1f', 'name' => 'origin/master'}, {'SHA1' => '08a2be82ba83c1e89e01f698a30203fb0284aa33', 'name' => 'origin/HEAD'}]}}}}], 'description' => 'just a test', 'displayName' => 'Test Project', 'url' => 'http://localhost:8080/job/Test%20Project/', 'builds' => [{'number' => 3, 'url' => 'http://localhost:8080/job/Test%20Project/3/'}, {'number' => 2, 'url' => 'http://localhost:8080/job/Test%20Project/2/'}, {'number' => 1, 'url' => 'http://localhost:8080/job/Test%20Project/1/'}], 'lastFailedBuild' => {'number' => 2, 'url' => 'http://localhost:8080/job/Test%20Project/2/'}, 'lastSuccessfulBuild' => {'number' => 3, 'url' => 'http://localhost:8080/job/Test%20Project/3/'}}
|
10
|
+
assert_equal('origin/master', @pm.get_value_of_key_from_json('name', json)) # find first occurrence
|
11
|
+
assert_equal('08a2be82ba83c1e89e01f698a30203fb0284aa33', @pm.get_value_of_key_from_json('SHA1', json)) # find first occurrence
|
12
|
+
assert_equal(3, @pm.get_value_of_key_from_json('number', json)) # find first occurrence
|
13
|
+
assert_equal('Test Project', @pm.get_value_of_key_from_json('displayName', json))
|
14
|
+
assert_equal('http://localhost:8080/job/Test%20Project/3/', @pm.get_value_of_key_from_json('lastSuccessfulBuild', json)['url'])
|
15
|
+
assert_equal(nil, @pm.get_value_of_key_from_json('sexyGirl', json)) # nonexistent key should return nil
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class TestServable < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@obj = Servable::PrivateMethods.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_is_https_request
|
9
|
+
assert_equal(true, @obj.is_https_request?('https://example.com/'))
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_is_not_https_request
|
13
|
+
assert_equal(false, @obj.is_https_request?('http://example.com/'))
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: agile_notifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jing Li
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-05-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.8.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.8.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httparty
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.11.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.11.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.14.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.14.0
|
55
|
+
description: agile_notifier alerts you via making wonderful noises, make software
|
56
|
+
development more fun
|
57
|
+
email:
|
58
|
+
- thyrlian@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- Gemfile
|
64
|
+
- Gemfile.lock
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- agile_notifier.gemspec
|
68
|
+
- lib/agile_notifier.rb
|
69
|
+
- lib/agile_notifier/ci.rb
|
70
|
+
- lib/agile_notifier/composer.rb
|
71
|
+
- lib/agile_notifier/configuration.rb
|
72
|
+
- lib/agile_notifier/git.rb
|
73
|
+
- lib/agile_notifier/github.rb
|
74
|
+
- lib/agile_notifier/its.rb
|
75
|
+
- lib/agile_notifier/jenkins.rb
|
76
|
+
- lib/agile_notifier/jira.rb
|
77
|
+
- lib/agile_notifier/judger.rb
|
78
|
+
- lib/agile_notifier/operating_system.rb
|
79
|
+
- lib/agile_notifier/player.rb
|
80
|
+
- lib/agile_notifier/response_helper.rb
|
81
|
+
- lib/agile_notifier/scm.rb
|
82
|
+
- lib/agile_notifier/servable.rb
|
83
|
+
- lib/agile_notifier/trackable.rb
|
84
|
+
- lib/agile_notifier/tts.rb
|
85
|
+
- test/ext/module.rb
|
86
|
+
- test/helper.rb
|
87
|
+
- test/test_operating_system.rb
|
88
|
+
- test/test_response_helper.rb
|
89
|
+
- test/test_servable.rb
|
90
|
+
homepage: https://github.com/thyrlian/AgileNotifier
|
91
|
+
licenses:
|
92
|
+
- MIT
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 2.1.4
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: agile_notifier alerts you via making wonderful noises
|
114
|
+
test_files:
|
115
|
+
- test/ext/module.rb
|
116
|
+
- test/helper.rb
|
117
|
+
- test/test_operating_system.rb
|
118
|
+
- test/test_response_helper.rb
|
119
|
+
- test/test_servable.rb
|