magi 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +12 -3
  3. data/Procfile +4 -0
  4. data/README.md +25 -1
  5. data/app/assets/stylesheets/application.css +1 -0
  6. data/app/assets/stylesheets/builds.scss +35 -0
  7. data/app/assets/stylesheets/common.scss +69 -0
  8. data/app/assets/stylesheets/jobs.scss +121 -0
  9. data/app/assets/stylesheets/layout.scss +19 -0
  10. data/app/assets/stylesheets/mixin.scss +6 -0
  11. data/app/controllers/application_controller.rb +16 -0
  12. data/app/controllers/builds_controller.rb +45 -0
  13. data/app/controllers/jobs_controller.rb +43 -0
  14. data/app/models/build.rb +39 -0
  15. data/app/models/job.rb +88 -0
  16. data/app/views/builds/show.html.slim +36 -0
  17. data/app/views/jobs/_form.html.slim +27 -0
  18. data/app/views/jobs/edit.html.slim +9 -0
  19. data/app/views/jobs/index.html.slim +19 -0
  20. data/app/views/jobs/new.html.slim +7 -0
  21. data/app/views/jobs/show.html.slim +24 -0
  22. data/app/views/layouts/application.html.slim +10 -0
  23. data/app/workers/build_worker.rb +8 -0
  24. data/bin/magi +6 -0
  25. data/config/application.rb +1 -1
  26. data/config/database.yml +21 -17
  27. data/config/routes.rb +4 -55
  28. data/db/migrate/20130531143239_create_jobs.rb +10 -0
  29. data/db/migrate/20130531155155_create_builds.rb +14 -0
  30. data/db/schema.rb +35 -0
  31. data/lib/magi/command.rb +53 -0
  32. data/lib/magi/executer.rb +31 -0
  33. data/lib/magi/scheduler.rb +55 -0
  34. data/lib/magi/version.rb +1 -1
  35. data/lib/magi.rb +1 -4
  36. data/magi.gemspec +29 -1
  37. data/script/clock.rb +5 -0
  38. data/spec/factories/build.rb +6 -0
  39. data/spec/factories/job.rb +7 -0
  40. data/spec/models/build_spec.rb +35 -0
  41. data/spec/models/job_spec.rb +54 -0
  42. data/spec/requests/builds_spec.rb +68 -0
  43. data/spec/requests/jobs_spec.rb +93 -0
  44. data/spec/spec_helper.rb +28 -0
  45. metadata +324 -50
  46. data/.gitignore +0 -19
  47. data/app/mailers/.gitkeep +0 -0
  48. data/app/models/.gitkeep +0 -0
  49. data/app/views/layouts/application.html.erb +0 -14
  50. data/doc/README_FOR_APP +0 -2
  51. data/lib/assets/.gitkeep +0 -0
  52. data/lib/tasks/.gitkeep +0 -0
  53. data/log/.gitkeep +0 -0
  54. data/public/index.html +0 -241
  55. data/test/fixtures/.gitkeep +0 -0
  56. data/test/functional/.gitkeep +0 -0
  57. data/test/integration/.gitkeep +0 -0
  58. data/test/performance/browsing_test.rb +0 -12
  59. data/test/test_helper.rb +0 -13
  60. data/test/unit/.gitkeep +0 -0
  61. data/vendor/assets/javascripts/.gitkeep +0 -0
  62. data/vendor/assets/stylesheets/.gitkeep +0 -0
  63. data/vendor/plugins/.gitkeep +0 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 912ef942160db8fe16a33b901a9213a40dc8482e
4
+ data.tar.gz: 5a3f6069f1c918788815dde909a74469a3a3e047
5
+ SHA512:
6
+ metadata.gz: 1b768a1e1a60ffcd0dcf7b8c7f78ffa004e5ffc4c64b395bc852a18ae964f638fe3520dd4331604dd80fda472de21b5c80ddf4b70506c1c88106c03f49a118a0
7
+ data.tar.gz: cedf3fd66a1b876a3f64a34dee6effe1ab0659b29603888508ab8a5ae2c0681ec2e4352c677fd14a1c54af95f3985f1b765f104e96147d39eba3c08355a0532a
data/Gemfile CHANGED
@@ -1,19 +1,28 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "rails", "3.2.13"
4
- gem "sqlite3"
3
+ gemspec
4
+
5
+ gem "font-awesome-rails"
5
6
  gem "jquery-rails"
7
+ gem "quiet_assets"
8
+ gem "sidekiq"
9
+ gem "slim"
6
10
 
7
11
  group :development, :test do
8
12
  gem "pry-rails"
9
13
  end
10
14
 
11
15
  group :test do
16
+ gem "factory_girl_rails"
17
+ gem "response_code_matchers"
18
+ gem "rspec-json_matcher"
12
19
  gem "rspec-rails"
20
+ gem "simplecov"
21
+ gem "thin"
13
22
  end
14
23
 
15
24
  group :assets do
16
- gem "sass-rails", "~> 3.2.3"
25
+ gem "sass-rails", "~> 3.2.3"
17
26
  gem "coffee-rails", "~> 3.2.1"
18
27
  gem "uglifier", ">= 1.0.3"
19
28
  end
data/Procfile ADDED
@@ -0,0 +1,4 @@
1
+ redis: redis-server
2
+ rails: bundle exec rails s
3
+ sidekiq: bundle exec sidekiq
4
+ clockwork: bundle exec clockwork script/clock.rb
data/README.md CHANGED
@@ -1,6 +1,30 @@
1
1
  # Magi
2
+ A continuous integration server implementation.
2
3
 
3
- ## Installation
4
+ ![](http://dl.dropbox.com/u/5978869/image/20130607_012059.png)
5
+
6
+ ## Install
4
7
  ```
5
8
  $ gem install magi
9
+ $ brew install mysql redis # or other commands to install MySQL and Redis
10
+ ```
11
+
12
+ ## Usage
13
+ ```
14
+ $ magi setup
15
+ $ magi start # open http://localhost:3000
6
16
  ```
17
+
18
+ ## Hack
19
+ Magi is just a rails application.
20
+ Feel free to do what you want.
21
+
22
+ ### tools
23
+ * Rails: HTTP server
24
+ * Sidekiq: background worker
25
+ * Redis: worker queue
26
+ * MySQL: store jobs & builds
27
+
28
+ ### todo
29
+ * plugin system
30
+ * cli options (e.g. http port, rails env, redis conf)
@@ -8,6 +8,7 @@
8
8
  * You're free to add application-wide styles to this file and they'll appear at the top of the
9
9
  * compiled file, but it's generally better to create a new file per style scope.
10
10
  *
11
+ *= require font-awesome
11
12
  *= require_self
12
13
  *= require_tree .
13
14
  */
@@ -0,0 +1,35 @@
1
+ @import "mixin";
2
+
3
+ section.build {
4
+ &.success h1 {
5
+ color: #27AE60;
6
+ }
7
+
8
+ &.failure h1 {
9
+ color: #C0392B;
10
+ }
11
+
12
+ &.success h1 span:before {
13
+ content: "\F058";
14
+ }
15
+
16
+ &.build.failure h1 span:before {
17
+ content: "\F057";
18
+ }
19
+
20
+ &.build.unfinished h1 span:before {
21
+ content: "\F056";
22
+ }
23
+
24
+ td {
25
+ padding-right: 1em;
26
+ }
27
+
28
+ pre {
29
+ @include box;
30
+ margin-top: 1em;
31
+ padding: .5em 1em;
32
+ background: #eee;
33
+ overflow-x: scroll;
34
+ }
35
+ }
@@ -0,0 +1,69 @@
1
+ @import "mixin";
2
+
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ body {
9
+ color: #333;
10
+ font-family: "Helvetica Neue", "Calibri", sans-serif;
11
+ line-height: 1.7;
12
+ }
13
+
14
+ a {
15
+ color: inherit;
16
+ text-decoration: none;
17
+ }
18
+
19
+ i {
20
+ margin-right: .3em;
21
+ }
22
+
23
+ textarea,
24
+ input {
25
+ font-size: 1em;
26
+ }
27
+
28
+ textarea,
29
+ input[type="text"] {
30
+ @include box;
31
+ width: 100%;
32
+ }
33
+
34
+ *:before {
35
+ margin-right: .3em;
36
+ font-family: FontAwesome;
37
+ }
38
+
39
+ .button {
40
+ @include box;
41
+ width: 10em;
42
+ font-size: 1em;
43
+ color: white;
44
+ background: #F39C12;
45
+ border-color: #F39C12;
46
+ line-height: inherit;
47
+ cursor: pointer;
48
+ text-align: center;
49
+ }
50
+
51
+ section.breadcrumbs {
52
+ ul {
53
+ @include box;
54
+ border-color: #eee;
55
+ background: #eee;
56
+ list-style: none;
57
+ overflow: hidden;
58
+ color: #888;
59
+ }
60
+
61
+ li {
62
+ float: left;
63
+ margin-right: 1em;
64
+ }
65
+
66
+ li.delimiter:before {
67
+ content: "\F105";
68
+ }
69
+ }
@@ -0,0 +1,121 @@
1
+ @import "mixin";
2
+
3
+ section.jobs {
4
+ h1:before {
5
+ content: "\F13D";
6
+ }
7
+
8
+ p {
9
+ color: #ccc;
10
+ }
11
+
12
+ ul {
13
+ margin-top: 1em;
14
+ }
15
+ }
16
+
17
+ section.job_settings {
18
+ h1:before {
19
+ content: "\F085";
20
+ }
21
+
22
+ td {
23
+ padding-right: 1em;
24
+ }
25
+
26
+ input[type="submit"] {
27
+ margin-top: 1em;
28
+ background: #34495e;
29
+ border-color: #34495e;
30
+ }
31
+ }
32
+
33
+ section.jobs,
34
+ section.builds {
35
+ .buttons {
36
+ overflow: hidden;
37
+ }
38
+
39
+ .buttons a {
40
+ float: left;
41
+ display: block;
42
+ margin-right: 1em;
43
+ }
44
+
45
+ .buttons a.new:before {
46
+ content: "\F040";
47
+ }
48
+
49
+ .buttons a.run:before {
50
+ content: "\F04B";
51
+ }
52
+
53
+ .buttons a.new,
54
+ .buttons a.settings {
55
+ background: #2C3E50;
56
+ border-color: #2C3E50;
57
+ }
58
+
59
+ .buttons a.settings:before {
60
+ content: "\F013";
61
+ }
62
+
63
+ .buttons a.delete {
64
+ background: #C0392B;
65
+ border-color: #C0392B;
66
+ }
67
+
68
+ .buttons a.delete:before {
69
+ content: "\F014";
70
+ }
71
+
72
+ .button {
73
+ margin-top: .5em;
74
+ }
75
+
76
+ ul {
77
+ margin-top: 1em;
78
+ list-style: none;
79
+ }
80
+
81
+ li a {
82
+ @include box;
83
+ display: block;
84
+ }
85
+
86
+ li a:hover {
87
+ background: #fafafa;
88
+ }
89
+
90
+ li.unfinished a:before {
91
+ content: "\F056";
92
+ }
93
+
94
+ li.success a:before {
95
+ content: "\F058";
96
+ color: #27AE60;
97
+ }
98
+
99
+ li.failure a:before {
100
+ content: "\F057";
101
+ color: #C0392B;
102
+ }
103
+
104
+ li.success a {
105
+ border-color: #27AE60;
106
+ }
107
+
108
+ li.failure a {
109
+ border-color: #C0392B;
110
+ }
111
+
112
+ li span {
113
+ margin-right: 1em;
114
+ }
115
+ }
116
+
117
+ section.builds {
118
+ h1:before {
119
+ content: "\F0FB";
120
+ }
121
+ }
@@ -0,0 +1,19 @@
1
+ header {
2
+ background: #333;
3
+
4
+ a {
5
+ color: white;
6
+ font-weight: normal;
7
+ }
8
+
9
+ .inner {
10
+ width: 920px;
11
+ margin: 0 auto;
12
+ }
13
+ }
14
+
15
+ section {
16
+ width: 920px;
17
+ margin: 0 auto;
18
+ padding: 20px;
19
+ }
@@ -0,0 +1,6 @@
1
+ @mixin box {
2
+ border-radius: .4em;
3
+ border: solid 2px #999;
4
+ padding: .2em .6em;
5
+ margin-bottom: .5em;
6
+ }
@@ -1,3 +1,19 @@
1
1
  class ApplicationController < ActionController::Base
2
2
  protect_from_forgery
3
+
4
+ respond_to :html, :json
5
+
6
+ private
7
+
8
+ def require_resource_params
9
+ head 400 unless resource_params
10
+ end
11
+
12
+ def resource_name
13
+ self.class.name.split("::").last.sub(/Controller$/, "").singularize.underscore
14
+ end
15
+
16
+ def resource_params
17
+ params[resource_name]
18
+ end
3
19
  end
@@ -0,0 +1,45 @@
1
+ class BuildsController < ApplicationController
2
+ before_filter :require_job
3
+ before_filter :require_resources, only: :index
4
+ before_filter :require_resource, only: [:show, :update, :destroy]
5
+ before_filter :require_resource_params, only: :update
6
+
7
+ def index
8
+ respond_with @resources
9
+ end
10
+
11
+ def show
12
+ respond_with @resource
13
+ end
14
+
15
+ def create
16
+ resource = @job.queue
17
+ respond_with resource, location: [@job, resource]
18
+ end
19
+
20
+ def update
21
+ respond_with @resource.update_attributes(resource_params.slice(:status))
22
+ end
23
+
24
+ def destroy
25
+ respond_with @resource.destroy
26
+ end
27
+
28
+ private
29
+
30
+ def require_job
31
+ @job = Job.find(params[:job_id])
32
+ end
33
+
34
+ def scope
35
+ @job.builds
36
+ end
37
+
38
+ def require_resources
39
+ @resources = scope
40
+ end
41
+
42
+ def require_resource
43
+ @resource = scope.find(params[:id])
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ class JobsController < ApplicationController
2
+ before_filter :require_resource_params, only: [:create, :update]
3
+ before_filter :require_resources, only: :index
4
+ before_filter :require_resource, only: [:show, :edit, :update, :destroy]
5
+
6
+ def index
7
+ respond_with @resources
8
+ end
9
+
10
+ def show
11
+ respond_with @resource
12
+ end
13
+
14
+ def new
15
+ respond_with @resource = scope.new
16
+ end
17
+
18
+ def create
19
+ respond_with @resource = scope.create_with_properties(resource_params)
20
+ end
21
+
22
+ def update
23
+ respond_with @resource.update_attributes_with_properties(resource_params)
24
+ end
25
+
26
+ def destroy
27
+ respond_with @resource.destroy
28
+ end
29
+
30
+ private
31
+
32
+ def scope
33
+ Job.scoped
34
+ end
35
+
36
+ def require_resources
37
+ @resources = scope
38
+ end
39
+
40
+ def require_resource
41
+ @resource = scope.find(params[:id])
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ class Build < ActiveRecord::Base
2
+ attr_accessible :finished_at, :started_at, :output, :status
3
+
4
+ validates :job_id, presence: true
5
+
6
+ belongs_to :job, touch: true
7
+
8
+ scope :recent, -> { order("created_at DESC") }
9
+
10
+ scope :finished, -> { where("finished_at IS NOT NULL") }
11
+
12
+ # Pushes this build to worker's queue.
13
+ def queue
14
+ BuildWorker.perform_async(id)
15
+ end
16
+
17
+ # Runs this build, usually called from BuildWorker#perform.
18
+ def start
19
+ update_attributes!(started_at: Time.now)
20
+ result = job.start
21
+ update_attributes!(finished_at: Time.now, status: result[:status], output: result[:output])
22
+ end
23
+
24
+ # Returns elapsed sec as a Float or nil.
25
+ def sec
26
+ finished_at - started_at if finished_at && started_at
27
+ end
28
+
29
+ def status_name
30
+ case status
31
+ when true
32
+ "success"
33
+ when false
34
+ "failure"
35
+ when nil
36
+ "unfinished"
37
+ end
38
+ end
39
+ end
data/app/models/job.rb ADDED
@@ -0,0 +1,88 @@
1
+ class Job < ActiveRecord::Base
2
+ attr_accessible :name, :config
3
+
4
+ serialize :config, Hash
5
+
6
+ validates :name, presence: true
7
+
8
+ has_many :builds
9
+
10
+ scope :recent, -> { order("updated_at DESC") }
11
+
12
+ delegate :status, :status_name, :status_icon_css_class, to: :last_finished_build, allow_nil: true
13
+
14
+ class << self
15
+ def create_with_properties(properties)
16
+ job = new
17
+ properties.each {|key, value| job.send("#{key}=", value) }
18
+ job.tap(&:save)
19
+ end
20
+
21
+ def queue
22
+ select(&:scheduled?).each(&:queue)
23
+ end
24
+
25
+ def property(name)
26
+ properties << name
27
+
28
+ define_method(name) do
29
+ config[name.to_s]
30
+ end
31
+
32
+ define_method("#{name}=") do |value|
33
+ config[name.to_s] = value
34
+ end
35
+ end
36
+
37
+ def properties
38
+ @properties ||= []
39
+ end
40
+ end
41
+
42
+ property(:description)
43
+
44
+ property(:schedule)
45
+
46
+ property(:script)
47
+
48
+ def start
49
+ script ? execute : raise_script_not_found
50
+ end
51
+
52
+ def scheduler
53
+ Magi::Scheduler.new(schedule)
54
+ end
55
+
56
+ def scheduled?
57
+ schedule && scheduler.scheduled?
58
+ end
59
+
60
+ def queue
61
+ builds.create.tap(&:queue)
62
+ end
63
+
64
+ def status_name
65
+ last_finished_build.try(:status_name) || "unfinished"
66
+ end
67
+
68
+ def last_finished_build
69
+ builds.finished.order(:finished_at).last
70
+ end
71
+
72
+ def update_attributes_with_properties(properties)
73
+ properties.each {|key, value| send("#{key}=", value) }
74
+ tap(&:save)
75
+ end
76
+
77
+ private
78
+
79
+ def execute
80
+ Magi::Executer.execute(script)
81
+ end
82
+
83
+ def raise_script_not_found
84
+ raise ScriptNotFound, 'You must set script["config"]'
85
+ end
86
+
87
+ class ScriptNotFound < StandardError; end
88
+ end
@@ -0,0 +1,36 @@
1
+ section.breadcrumbs
2
+ ul
3
+ li= link_to "Home", jobs_path
4
+ li.delimiter
5
+ li= link_to @resource.job.name, @resource.job
6
+ li.delimiter
7
+ li= link_to "##{@resource.id}", [@resource.job, @resource]
8
+
9
+ section.build class = @resource.status_name
10
+ h1
11
+ span ##{@resource.id}
12
+
13
+ table
14
+ tr
15
+ td started_at
16
+ td= ":"
17
+ td= @resource.started_at
18
+
19
+ tr
20
+ td finished_at
21
+ td= ":"
22
+ td= @resource.finished_at
23
+
24
+ tr
25
+ td sec
26
+ td= ":"
27
+ td= @resource.sec
28
+
29
+ tr
30
+ td status
31
+ td= ":"
32
+ td= @resource.status_name
33
+
34
+ pre
35
+ code
36
+ = @resource.output
@@ -0,0 +1,27 @@
1
+ section.job_settings
2
+ h1 Settings
3
+
4
+ = form_for resource do |f|
5
+ .field
6
+ p= f.label :name
7
+ p= f.text_field :name, placeholder: "required"
8
+
9
+ .field
10
+ p= f.label :description
11
+ p= f.text_area :description, rows: 3
12
+
13
+ .field
14
+ p= f.label :script
15
+ p= f.text_area :script, rows: 6
16
+
17
+ .field
18
+ p= f.label :schedule
19
+ p= f.text_field :schedule, placeholder: "* * * * *"
20
+
21
+ - (resource.class.properties - [:description, :schedule, :script]).each do |name|
22
+ .field
23
+ p= f.label name
24
+ p= f.text_field name
25
+
26
+ .field
27
+ p= f.submit class: "button"
@@ -0,0 +1,9 @@
1
+ section.breadcrumbs
2
+ ul
3
+ li= link_to "Home", jobs_path
4
+ li.delimiter
5
+ li= link_to @resource.name, @resource
6
+ li.delimiter
7
+ li= link_to "settings", edit_job_path(@resource)
8
+
9
+ = render "form", resource: @resource
@@ -0,0 +1,19 @@
1
+ section.breadcrumbs
2
+ ul
3
+ li= link_to "Home", jobs_path
4
+
5
+ section.jobs
6
+ h1 JOBS
7
+
8
+ p #{Job.count} jobs are found.
9
+
10
+
11
+ .buttons
12
+ = link_to "New", [:new, :job], class: "button new"
13
+
14
+ ul
15
+ - @resources.recent.each do |resource|
16
+ li class = resource.status_name
17
+ = link_to resource do
18
+ span
19
+ = resource.name
@@ -0,0 +1,7 @@
1
+ section.breadcrumbs
2
+ ul
3
+ li= link_to "Home", jobs_path
4
+ li.delimiter
5
+ li= link_to "Create Job", @resource
6
+
7
+ = render "form", resource: @resource
@@ -0,0 +1,24 @@
1
+ section.breadcrumbs
2
+ ul
3
+ li= link_to "Home", jobs_path
4
+ li.delimiter
5
+ li= link_to @resource.name, @resource
6
+
7
+ section.builds
8
+ h1
9
+ = @resource.name
10
+
11
+ p
12
+ = @resource.description || "(no description)"
13
+
14
+ .buttons
15
+ = link_to "Run", [@resource, :builds], method: "POST", class: "button run"
16
+ = link_to "Settings", [:edit, @resource], class: "button settings"
17
+ = link_to "Delete", @resource, method: "DELETE", confirm: "Are you sure?", class: "button delete"
18
+
19
+ ul
20
+ - @resource.builds.recent.limit(10).each do |build|
21
+ li class = build.status_name
22
+ = link_to [@resource, build] do
23
+ span.id ##{build.id}
24
+ span= build.finished_at