deep_thought 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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