magi 0.0.1 → 0.0.2

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 (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