deep_thought 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +184 -0
  5. data/Rakefile +50 -0
  6. data/config.ru +5 -0
  7. data/db/migrate/20130414012805_create_projects.rb +15 -0
  8. data/db/migrate/20130426165010_create_states.rb +14 -0
  9. data/db/migrate/20130501003758_create_users.rb +14 -0
  10. data/db/migrate/20130508001711_add_api_key_to_users.rb +9 -0
  11. data/db/migrate/20130509014935_create_deploys.rb +27 -0
  12. data/db/migrate/20130514062148_create_delayed_jobs.rb +21 -0
  13. data/db/migrate/20130516042149_change_delayed_job_last_error_to_text.rb +9 -0
  14. data/db/migrate/20130517010210_add_ci_to_projects.rb +9 -0
  15. data/db/migrate/20130525001332_add_notification_url_to_users.rb +9 -0
  16. data/db/migrate/20131127022704_remove_deploy_type_and_ci_from_projects.rb +11 -0
  17. data/db/schema.rb +72 -0
  18. data/deep_thought.gemspec +45 -0
  19. data/lib/deep_thought.rb +62 -0
  20. data/lib/deep_thought/api.rb +156 -0
  21. data/lib/deep_thought/app.rb +342 -0
  22. data/lib/deep_thought/ci_service.rb +39 -0
  23. data/lib/deep_thought/ci_service/janky.rb +34 -0
  24. data/lib/deep_thought/deployer.rb +180 -0
  25. data/lib/deep_thought/deployer/deployer.rb +17 -0
  26. data/lib/deep_thought/deployer/shell.rb +45 -0
  27. data/lib/deep_thought/git.rb +76 -0
  28. data/lib/deep_thought/models/deploy.rb +27 -0
  29. data/lib/deep_thought/models/project.rb +32 -0
  30. data/lib/deep_thought/models/state.rb +6 -0
  31. data/lib/deep_thought/models/user.rb +45 -0
  32. data/lib/deep_thought/notifier.rb +13 -0
  33. data/lib/deep_thought/public/assets/javascripts/jquery.min.js +6 -0
  34. data/lib/deep_thought/public/assets/javascripts/main.js +36 -0
  35. data/lib/deep_thought/public/assets/stylesheets/main.css +438 -0
  36. data/lib/deep_thought/public/robots.txt +2 -0
  37. data/lib/deep_thought/scaler.rb +35 -0
  38. data/lib/deep_thought/tasks.rb +65 -0
  39. data/lib/deep_thought/version.rb +3 -0
  40. data/lib/deep_thought/views/history/index.haml +4 -0
  41. data/lib/deep_thought/views/history/show.haml +68 -0
  42. data/lib/deep_thought/views/layouts/layout.haml +32 -0
  43. data/lib/deep_thought/views/projects/edit.haml +15 -0
  44. data/lib/deep_thought/views/projects/index.haml +7 -0
  45. data/lib/deep_thought/views/projects/new.haml +8 -0
  46. data/lib/deep_thought/views/projects/show.haml +25 -0
  47. data/lib/deep_thought/views/sessions/login.haml +8 -0
  48. data/lib/deep_thought/views/users/index.haml +7 -0
  49. data/lib/deep_thought/views/users/new.haml +12 -0
  50. data/lib/deep_thought/views/users/show.haml +38 -0
  51. data/script/bootstrap +3 -0
  52. data/script/console +7 -0
  53. data/script/server +7 -0
  54. data/script/test +3 -0
  55. data/test/deep_thought_api_test.rb +114 -0
  56. data/test/deep_thought_app_test.rb +315 -0
  57. data/test/deep_thought_ci_service_test.rb +44 -0
  58. data/test/deep_thought_deployer_test.rb +72 -0
  59. data/test/deep_thought_git_test.rb +62 -0
  60. data/test/deep_thought_janky_test.rb +42 -0
  61. data/test/deep_thought_notifier_test.rb +35 -0
  62. data/test/deep_thought_project_test.rb +34 -0
  63. data/test/deep_thought_shell_test.rb +38 -0
  64. data/test/fixtures/project-test/HEAD +1 -0
  65. data/test/fixtures/project-test/config +8 -0
  66. data/test/fixtures/project-test/description +1 -0
  67. data/test/fixtures/project-test/hooks/applypatch-msg.sample +15 -0
  68. data/test/fixtures/project-test/hooks/commit-msg.sample +24 -0
  69. data/test/fixtures/project-test/hooks/post-update.sample +8 -0
  70. data/test/fixtures/project-test/hooks/pre-applypatch.sample +14 -0
  71. data/test/fixtures/project-test/hooks/pre-commit.sample +49 -0
  72. data/test/fixtures/project-test/hooks/pre-push.sample +54 -0
  73. data/test/fixtures/project-test/hooks/pre-rebase.sample +169 -0
  74. data/test/fixtures/project-test/hooks/prepare-commit-msg.sample +36 -0
  75. data/test/fixtures/project-test/hooks/update.sample +128 -0
  76. data/test/fixtures/project-test/info/exclude +6 -0
  77. data/test/fixtures/project-test/objects/13/8b8baba400e64fc384d468e80f10cf707e38d7 +1 -0
  78. data/test/fixtures/project-test/objects/6b/8fb16e1675767742afc70ac2ce038da46461f8 +0 -0
  79. data/test/fixtures/project-test/objects/7f/fc496d150b8a0015e3a16be8bacd47daed1a1c +0 -0
  80. data/test/fixtures/project-test/objects/8f/8606174a2aa11545d119fe3c4d6840fe0c4825 +0 -0
  81. data/test/fixtures/project-test/objects/9c/362942288a8ff6dc1addf7c5199bd9837e09de +0 -0
  82. data/test/fixtures/project-test/objects/b5/f7341de6145d3f8b95877cd2675d798fd172f1 +0 -0
  83. data/test/fixtures/project-test/objects/d1/f1e8664b0326316fb18796ac4ae1171e239cb6 +1 -0
  84. data/test/fixtures/project-test/objects/e6/d7ad6d61242936a2cd2bb9ae236adf43e59718 +0 -0
  85. data/test/fixtures/project-test/objects/fa/4cf5ff52462d17b6378a06fae8771c13b654b0 +1 -0
  86. data/test/fixtures/project-test/objects/fa/dda6bc20afa5cf93196bfb08a4989ee3b75ed7 +0 -0
  87. data/test/fixtures/project-test/packed-refs +4 -0
  88. data/test/fixtures/project-test/refs/heads/.gitkeep +0 -0
  89. data/test/fixtures/project-test/refs/tags/.gitkeep +0 -0
  90. data/test/test_helper.rb +42 -0
  91. metadata +462 -0
@@ -0,0 +1,39 @@
1
+ module DeepThought
2
+ module CIService
3
+ class CIServiceNotFoundError < StandardError; end
4
+ class CIBuildNotGreenError < StandardError; end
5
+ class CIProjectAccessError < StandardError; end
6
+
7
+ class << self
8
+ attr_accessor :adapters, :ci_service
9
+ end
10
+
11
+ def self.adapters
12
+ @adapters ||= {}
13
+ end
14
+
15
+ def self.register_adapter(name, service)
16
+ self.adapters[name] = service
17
+ end
18
+
19
+ def self.setup(settings)
20
+ if settings['CI_SERVICE']
21
+ if @adapters.keys.include?(settings['CI_SERVICE'])
22
+ klass = adapters[settings['CI_SERVICE']]
23
+ @ci_service = klass.new
24
+ @ci_service.setup(settings)
25
+ else
26
+ raise CIServiceNotFoundError, "I don't have a CI service called \"#{settings['CI_SERVICE']}\"."
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.is_branch_green?(app, branch, hash)
32
+ begin
33
+ @ci_service.is_branch_green?(app, branch, hash)
34
+ rescue
35
+ raise CIProjectAccessError, "Something went wrong asking #{ENV['CI_SERVICE']} about commit #{hash} in #{app} on the #{branch} branch."
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ require 'httparty'
2
+
3
+ module DeepThought
4
+ module CIService
5
+ class Janky
6
+ attr_accessor :endpoint, :username, :password
7
+
8
+ def setup(settings)
9
+ @endpoint = settings['CI_SERVICE_ENDPOINT']
10
+ @username = settings['CI_SERVICE_USERNAME']
11
+ @password = settings['CI_SERVICE_PASSWORD']
12
+ end
13
+
14
+ def is_branch_green?(app, branch, hash)
15
+ is_green = false
16
+
17
+ response = HTTParty.get("#{@endpoint}/_hubot/#{app}/#{branch}", {:basic_auth => {:username => @username, :password => @password}})
18
+ builds = JSON.parse(response.body)
19
+
20
+ builds.each do |build|
21
+ if build['sha1'].to_s == hash.to_s
22
+ if build['green']
23
+ is_green = true
24
+ end
25
+
26
+ break
27
+ end
28
+ end
29
+
30
+ is_green
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,180 @@
1
+ require 'yaml'
2
+ require 'deep_thought/git'
3
+
4
+ module DeepThought
5
+ module Deployer
6
+ class DeployerNotFoundError < StandardError; end
7
+ class DeploymentFailedError < StandardError; end
8
+ class DeploymentInProgressError < StandardError; end
9
+
10
+ class << self
11
+ attr_accessor :adapters
12
+ end
13
+
14
+ def self.adapters
15
+ @adapters ||= {}
16
+ end
17
+
18
+ def self.register_adapter(name, service)
19
+ self.adapters[name] = service
20
+ end
21
+
22
+ def self.create(project, user, via, options = {})
23
+ branch = options[:branch] || 'master'
24
+ actions = options[:actions] if options[:actions]
25
+ environment = options[:environment] if options[:environment]
26
+ box = options[:box] if options[:box]
27
+ variables = options[:variables] if options[:variables]
28
+ on_behalf_of = options[:on_behalf_of] if options[:on_behalf_of]
29
+
30
+ if is_deploying?
31
+ raise DeploymentInProgressError, "Sorry, but I'm currently in mid-deployment. Ask me again when I'm done."
32
+ end
33
+
34
+ project.setup
35
+
36
+ hash = Git.get_latest_commit_for_branch(project, branch)
37
+
38
+ project_config = YAML.load_file(".projects/#{project.name}/.deepthought.yml")
39
+
40
+ if project_config['ci']
41
+ uses_ci = project_config['ci']['enabled'] || false
42
+ else
43
+ uses_ci = false
44
+ end
45
+
46
+ if DeepThought::CIService.ci_service
47
+ if uses_ci
48
+ ci_project_name = project_config['ci']['name'] || project.name
49
+ if !DeepThought::CIService.is_branch_green?(ci_project_name, branch, hash)
50
+ raise DeepThought::CIService::CIBuildNotGreenError, "Commit #{hash} on project #{app} (in branch #{branch}) is not green. Fix it before deploying."
51
+ end
52
+ end
53
+ end
54
+
55
+ deploy = DeepThought::Deploy.new
56
+ deploy.project_id = project.id
57
+ deploy.user_id = user.id
58
+ deploy.branch = branch
59
+ deploy.commit = hash.to_s
60
+ deploy.via = via
61
+ deploy.actions = actions.to_yaml if actions
62
+ deploy.environment = environment if environment
63
+ deploy.box = box if environment && box
64
+ deploy.variables = variables.to_yaml if variables
65
+ deploy.on_behalf_of = on_behalf_of if on_behalf_of
66
+ deploy.save!
67
+ end
68
+
69
+ def self.execute(deploy)
70
+ if !is_deploying?
71
+ Git.switch_to_branch(deploy.project, deploy.branch)
72
+
73
+ deploy.project.setup
74
+
75
+ project_config = YAML.load_file(".projects/#{deploy.project.name}/.deepthought.yml") || {}
76
+ deploy_type = project_config['deploy_type'] || 'shell'
77
+
78
+ if @adapters.keys.include?(deploy_type)
79
+ lock_deployer
80
+
81
+ deploy.started_at = DateTime.now
82
+ deploy.in_progress = true
83
+ deploy.save!
84
+
85
+ klass = adapters[deploy_type]
86
+ deployer = klass.new
87
+ deployer.setup(deploy.project, project_config)
88
+ deploy_status = deployer.execute(deploy, project_config)
89
+
90
+ unlock_deployer
91
+
92
+ deploy.finished_at = DateTime.now
93
+ deploy.in_progress = false
94
+
95
+ deploy_summary = deploy.project.name
96
+
97
+ if deploy.actions
98
+ actions = YAML.load(deploy.actions)
99
+ actions.each do |action|
100
+ deploy_summary += "/#{action}"
101
+ end
102
+ end
103
+
104
+ deploy_summary += " #{deploy.branch}"
105
+
106
+ if deploy.environment
107
+ deploy_summary += " to #{deploy.environment}"
108
+
109
+ if deploy.box
110
+ deploy_summary += "/#{deploy.box}"
111
+ end
112
+ end
113
+
114
+ if deploy.variables
115
+ variables = YAML.load(deploy.variables)
116
+ variables.each do |k, v|
117
+ deploy_summary += " #{k}=#{v}"
118
+ end
119
+ end
120
+
121
+ if deploy_status
122
+ deploy.was_successful = true
123
+ deploy.save!
124
+
125
+ DeepThought::Notifier.notify(deploy.user, "SUCCESS: #{deploy_summary}")
126
+
127
+ true
128
+ else
129
+ deploy.was_successful = false
130
+ deploy.save!
131
+
132
+ DeepThought::Notifier.notify(deploy.user, "FAILED: #{deploy_summary}")
133
+
134
+ raise DeploymentFailedError, "The deployment pondered its own short existence before hitting the ground with a sudden wet thud."
135
+ end
136
+ else
137
+ raise DeployerNotFoundError, "I don't have a deployer called \"#{deploy_type}\"."
138
+ end
139
+ else
140
+ raise DeploymentInProgressError, "Sorry, but I'm currently in mid-deployment. Ask me again when I'm done."
141
+ end
142
+ end
143
+
144
+ def self.is_deploying?
145
+ deployer_state = get_or_create_deployer_state
146
+
147
+ if deployer_state.state == 'true'
148
+ true
149
+ else
150
+ false
151
+ end
152
+ end
153
+
154
+ def self.lock_deployer
155
+ deployer_state = get_or_create_deployer_state
156
+
157
+ deployer_state.state = 'true'
158
+ deployer_state.save
159
+ end
160
+
161
+ def self.unlock_deployer
162
+ deployer_state = get_or_create_deployer_state
163
+
164
+ deployer_state.state = 'false'
165
+ deployer_state.save
166
+ end
167
+
168
+ private
169
+
170
+ def self.get_or_create_deployer_state
171
+ deployer_state = DeepThought::State.find_by_name('deployer')
172
+
173
+ if !deployer_state
174
+ deployer_state = DeepThought::State.create(:name => 'deployer', :state => 'false')
175
+ end
176
+
177
+ deployer_state
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,17 @@
1
+ module DeepThought
2
+ module Deployer
3
+ class Deployer
4
+ def initialize
5
+ if self.class.name == 'DeepThought::Deployer::Deployer'
6
+ raise "#{self.class.name} is abstract, you cannot instantiate it directly."
7
+ end
8
+ end
9
+
10
+ def setup(project, config)
11
+ end
12
+
13
+ def execute(deploy, config)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ require 'deep_thought/deployer/deployer'
2
+
3
+ module DeepThought
4
+ module Deployer
5
+ class Shell < DeepThought::Deployer::Deployer
6
+ def execute(deploy, config)
7
+ environment = deploy.environment || "development"
8
+
9
+ root = config['root'] || "script/deploy"
10
+
11
+ command = "#{root} #{environment} deploy"
12
+
13
+ if deploy.actions
14
+ actions = YAML.load(deploy.actions)
15
+ actions.each do |action|
16
+ command += ":#{action}"
17
+ end
18
+ end
19
+
20
+ command += " branch=#{deploy.branch}"
21
+ command += " box=#{deploy.box}" if deploy.box
22
+
23
+ if deploy.variables
24
+ variables = YAML.load(deploy.variables)
25
+ variables.each do |k, v|
26
+ command += " #{k}=#{v}"
27
+ end
28
+ end
29
+
30
+ commands = []
31
+
32
+ commands << "cd ./.projects/#{deploy.project.name}"
33
+ commands << "#{command} 2>&1"
34
+
35
+ executable = commands.join(" && ")
36
+
37
+ log = `#{executable}`
38
+
39
+ deploy.log = log
40
+
41
+ $?.success?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,76 @@
1
+ require 'rugged'
2
+
3
+ module DeepThought
4
+ module Git
5
+ class GitRepositoryNotFoundError < StandardError; end
6
+ class GitBranchNotFoundError < StandardError; end
7
+
8
+ def self.setup(project)
9
+ if !File.directory?(".projects/#{project.name}/.git")
10
+ exit_status = system "git clone #{project.repo_url} .projects/#{project.name} > /dev/null 2>&1"
11
+
12
+ if exit_status
13
+ if !File.directory?(".projects/#{project.name}/.git")
14
+ false
15
+ else
16
+ if Dir.entries(".projects/#{project.name}") == [".", "..", ".git"]
17
+ false
18
+ else
19
+ true
20
+ end
21
+ end
22
+ else
23
+ false
24
+ end
25
+ else
26
+ true
27
+ end
28
+ end
29
+
30
+ def self.get_list_of_branches(project)
31
+ self.clone_if_not_exists(project)
32
+
33
+ system "cd ./.projects/#{project.name} && git fetch -p > /dev/null 2>&1"
34
+
35
+ repo = Rugged::Repository.new(".projects/#{project.name}")
36
+
37
+ branches = Rugged::Branch.each_name(repo, :remote).sort
38
+ branches.map! { |x| x.sub!('origin/', '') }
39
+ branches.delete('HEAD')
40
+
41
+ branches
42
+ end
43
+
44
+ def self.get_latest_commit_for_branch(project, branch)
45
+ self.clone_if_not_exists(project)
46
+
47
+ system "cd ./.projects/#{project.name} && git fetch -p > /dev/null 2>&1"
48
+
49
+ repo = Rugged::Repository.new(".projects/#{project.name}")
50
+
51
+ switch_to_branch(project, branch)
52
+
53
+ repo.head.target
54
+ end
55
+
56
+ def self.switch_to_branch(project, branch)
57
+ self.clone_if_not_exists(project)
58
+
59
+ exit_status = system "cd ./.projects/#{project.name} && git fetch -p > /dev/null 2>&1 && git reset --hard origin/#{branch} > /dev/null 2>&1"
60
+
61
+ if exit_status
62
+ true
63
+ else
64
+ raise GitBranchNotFoundError, "#{project.name} doesn't appear to have a branch called #{branch}. Have you pushed it?"
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def self.clone_if_not_exists(project)
71
+ if !self.setup(project)
72
+ raise DeepThought::Git::GitRepositoryNotFoundError, "I can't seem to access that repo. Are you sure the URL is correct and that I have access to it?"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,27 @@
1
+ module DeepThought
2
+ class Deploy < ActiveRecord::Base
3
+ belongs_to :project
4
+ belongs_to :user
5
+
6
+ after_create :queue
7
+
8
+ validates_presence_of :branch
9
+ validates_presence_of :commit
10
+ validates_presence_of :project
11
+ validates_presence_of :user
12
+
13
+ def queue
14
+ Delayed::Job.enqueue self
15
+
16
+ DeepThought::Scaler.scale
17
+ end
18
+
19
+ def perform
20
+ DeepThought::Deployer.execute(self)
21
+ end
22
+
23
+ def max_attempts
24
+ return 1
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ require 'fileutils'
2
+
3
+ module DeepThought
4
+ class ProjectConfigNotFoundError < StandardError; end
5
+
6
+ class Project < ActiveRecord::Base
7
+ has_many :deploys
8
+
9
+ before_destroy :delete_repo
10
+
11
+ validates :name, presence: true, uniqueness: true
12
+ validates :repo_url, presence: true
13
+
14
+ def setup
15
+ if DeepThought::Git.setup(self)
16
+ if !File.exists?(".projects/#{self.name}/.deepthought.yml")
17
+ delete_repo
18
+
19
+ raise DeepThought::ProjectConfigNotFoundError, "#{self.name} does not appear to have a .deepthought.yml config file. Add one and try again."
20
+ end
21
+ else
22
+ raise DeepThought::Git::GitRepositoryNotFoundError, "I can't seem to access that repo. Are you sure the URL is correct and that I have access to it?"
23
+ end
24
+ end
25
+
26
+ def delete_repo
27
+ if File.directory?(".projects/#{self.name}")
28
+ FileUtils.rm_rf(".projects/#{self.name}")
29
+ end
30
+ end
31
+ end
32
+ end