shipit-engine 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/app/assets/javascripts/shipit/page_updater.js.coffee +63 -0
  4. data/app/assets/javascripts/shipit/stacks.js.coffee +9 -21
  5. data/app/assets/stylesheets/_base/_base.scss +2 -2
  6. data/app/assets/stylesheets/_base/_colors.scss +0 -1
  7. data/app/assets/stylesheets/_base/_forms.scss +14 -0
  8. data/app/assets/stylesheets/_pages/_commits.scss +16 -6
  9. data/app/assets/stylesheets/_pages/_settings.scss +8 -0
  10. data/app/assets/stylesheets/_pages/_stacks.scss +1 -1
  11. data/app/controllers/shipit/api/base_controller.rb +7 -3
  12. data/app/controllers/shipit/api/ccmenu_controller.rb +33 -0
  13. data/app/controllers/shipit/api/pull_requests_controller.rb +36 -0
  14. data/app/controllers/shipit/api/stacks_controller.rb +1 -0
  15. data/app/controllers/shipit/ccmenu_url_controller.rb +22 -0
  16. data/app/controllers/shipit/pull_requests_controller.rb +30 -0
  17. data/app/controllers/shipit/stacks_controller.rb +7 -2
  18. data/app/controllers/shipit/webhooks_controller.rb +1 -2
  19. data/app/helpers/shipit/github_url_helper.rb +8 -2
  20. data/app/helpers/shipit/shipit_helper.rb +9 -0
  21. data/app/helpers/shipit/stacks_helper.rb +22 -7
  22. data/app/jobs/shipit/background_job/unique.rb +19 -1
  23. data/app/jobs/shipit/cache_deploy_spec_job.rb +1 -1
  24. data/app/jobs/shipit/merge_pull_requests_job.rb +26 -0
  25. data/app/jobs/shipit/perform_task_job.rb +1 -1
  26. data/app/jobs/shipit/refresh_pull_request_job.rb +8 -0
  27. data/app/models/concerns/shipit/deferred_touch.rb +6 -1
  28. data/app/models/shipit/anonymous_user.rb +4 -0
  29. data/app/models/shipit/application_record.rb +5 -0
  30. data/app/models/shipit/commit.rb +51 -49
  31. data/app/models/shipit/commit_message.rb +32 -0
  32. data/app/models/shipit/deploy.rb +5 -0
  33. data/app/models/shipit/deploy_spec.rb +26 -1
  34. data/app/models/shipit/deploy_spec/file_system.rb +6 -1
  35. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +10 -13
  36. data/app/models/shipit/deploy_spec/npm_discovery.rb +2 -1
  37. data/app/models/shipit/duration.rb +3 -1
  38. data/app/models/shipit/hook.rb +1 -0
  39. data/app/models/shipit/pull_request.rb +252 -0
  40. data/app/models/shipit/stack.rb +33 -17
  41. data/app/models/shipit/status.rb +1 -16
  42. data/app/models/shipit/status/common.rb +45 -0
  43. data/app/models/shipit/status/group.rb +82 -0
  44. data/app/models/shipit/status/missing.rb +30 -0
  45. data/app/models/shipit/status/unknown.rb +33 -0
  46. data/app/models/shipit/unlimited_api_client.rb +10 -0
  47. data/app/serializers/shipit/commit_serializer.rb +1 -1
  48. data/app/serializers/shipit/pull_request_serializer.rb +20 -0
  49. data/app/serializers/shipit/stack_serializer.rb +6 -2
  50. data/app/views/layouts/shipit.html.erb +41 -39
  51. data/app/views/shipit/ccmenu/project.xml.builder +13 -0
  52. data/app/views/shipit/commits/_commit.html.erb +1 -1
  53. data/app/views/shipit/deploys/_deploy.html.erb +1 -1
  54. data/app/views/shipit/pull_requests/_pull_request.html.erb +29 -0
  55. data/app/views/shipit/pull_requests/index.html.erb +20 -0
  56. data/app/views/shipit/shared/_author.html.erb +7 -0
  57. data/app/views/shipit/stacks/_header.html.erb +5 -0
  58. data/app/views/shipit/stacks/settings.html.erb +13 -0
  59. data/app/views/shipit/stacks/show.html.erb +3 -2
  60. data/app/views/shipit/statuses/_group.html.erb +1 -1
  61. data/app/views/shipit/tasks/_task.html.erb +1 -1
  62. data/config/initializers/inflections.rb +3 -0
  63. data/config/locales/en.yml +1 -3
  64. data/config/routes.rb +8 -0
  65. data/db/migrate/20170130113633_create_shipit_pull_requests.rb +25 -0
  66. data/db/migrate/20170208143657_add_pull_request_number_and_title_to_commits.rb +7 -0
  67. data/db/migrate/20170208154609_backfill_merge_commits.rb +13 -0
  68. data/db/migrate/20170209160355_add_branch_to_pull_requests.rb +5 -0
  69. data/db/migrate/20170215123538_add_merge_queue_enabled_to_stacks.rb +5 -0
  70. data/db/migrate/20170220152410_improve_users_indexing.rb +6 -0
  71. data/db/migrate/20170221102128_improve_tasks_indexing.rb +8 -0
  72. data/db/migrate/20170221130336_add_last_revalidated_at_on_pull_requests.rb +10 -0
  73. data/lib/shipit.rb +2 -0
  74. data/lib/shipit/version.rb +1 -1
  75. data/lib/tasks/cron.rake +1 -0
  76. data/test/controllers/api/ccmenu_controller_test.rb +57 -0
  77. data/test/controllers/api/commits_controller_test.rb +1 -1
  78. data/test/controllers/api/pull_requests_controller_test.rb +59 -0
  79. data/test/controllers/ccmenu_controller_test.rb +33 -0
  80. data/test/controllers/pull_requests_controller_test.rb +31 -0
  81. data/test/controllers/webhooks_controller_test.rb +3 -4
  82. data/test/dummy/config/environments/development.rb +3 -1
  83. data/test/dummy/data/stacks/shopify/junk/production/git/README.md +8 -0
  84. data/test/dummy/data/stacks/shopify/junk/production/git/circle.yml +4 -0
  85. data/test/dummy/data/stacks/shopify/junk/production/git/shipit.yml +4 -0
  86. data/test/dummy/db/development.sqlite3 +0 -0
  87. data/test/dummy/db/schema.rb +45 -11
  88. data/test/dummy/db/seeds.rb +33 -10
  89. data/test/dummy/db/test.sqlite3 +0 -0
  90. data/test/fixtures/shipit/commits.yml +14 -0
  91. data/test/fixtures/shipit/pull_requests.yml +56 -0
  92. data/test/fixtures/shipit/stacks.yml +5 -1
  93. data/test/fixtures/shipit/statuses.yml +8 -0
  94. data/test/helpers/json_helper.rb +16 -14
  95. data/test/jobs/merge_pull_requests_job_test.rb +59 -0
  96. data/test/models/commits_test.rb +104 -49
  97. data/test/{unit → models}/deploy_spec_test.rb +138 -12
  98. data/test/models/deploys_test.rb +10 -4
  99. data/test/models/pull_request_test.rb +197 -0
  100. data/test/models/stacks_test.rb +46 -53
  101. data/test/models/status/group_test.rb +44 -0
  102. data/test/models/status/missing_test.rb +23 -0
  103. data/test/models/status_test.rb +3 -6
  104. data/test/unit/csv_serializer_test.rb +10 -2
  105. metadata +57 -12
  106. data/app/models/shipit/missing_status.rb +0 -21
  107. data/app/models/shipit/status_group.rb +0 -35
  108. data/app/models/shipit/unknown_status.rb +0 -48
  109. data/app/views/shipit/commits/_commit_author.html.erb +0 -7
  110. data/test/models/missing_status_test.rb +0 -23
  111. data/test/models/status_group_test.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1ed7609bec0e259e4962046f8d6107263c7429db
4
- data.tar.gz: 835c09964fcd482736103a6d8e40f59bd2aa1f0e
3
+ metadata.gz: 728cef09e4ae36c728e2a5d933f5967f4dcbe757
4
+ data.tar.gz: e35599675982e893d70e0c25d678da7e8910e7f4
5
5
  SHA512:
6
- metadata.gz: cacebc0ec7fcce7a0af8feefa79d7d48878548bd07b8a75b02878383ed89d7db4364e9b623f910bf9090b5bbbd7e4d201926f86309f265ef279ac40a1b554ee9
7
- data.tar.gz: 6ef2234ef1e1be3d462572a817edeab7ad042a0ca47639834ad349b88308a7d9541c0d3527f63697752735b9e4c3528b597197de2e882c569db9b9ef9fbc6521
6
+ metadata.gz: bc3ecc88e8e13e99dca6f5789d364de99a21374fa566cd0273ed013f2f2b8da1c9a007fe9931daa76f66c12ba0ff9890cd2c8b4110d28f35510a7e3d29a3cef0
7
+ data.tar.gz: fde31379bbe98eb1dfb6af272a54a1b6b9e1a3e171d1dcc1588f8e67a257fe633117d1c35251f3543d3a5d585039c68e0e21dcd2ce8f945f009deee0bd722695
data/README.md CHANGED
@@ -268,9 +268,10 @@ deploy:
268
268
  ```
269
269
  <br>
270
270
 
271
- **<code>deploy.max_commits</code>** allows you to set a limit to the number of commits being shipped per deploys.
271
+ **<code>deploy.max_commits</code>** define the maximum number of commits that should be shipped per deploys. Defaults to `8`.
272
272
 
273
273
  Human users will be warned that they are not respecting the recommendation, but allowed to continue.
274
+ However continuous delivery will respect this limit. If there is no deployable commits in this range, a human intervention will be required.
274
275
 
275
276
  For example:
276
277
 
@@ -393,6 +394,38 @@ ci:
393
394
  - ci/circleci
394
395
  ```
395
396
 
397
+ <h3 id="merge-queue">Merge Queue</h3>
398
+
399
+ The merge queue allow to register pull requests for them to be merged by Shipit once the stack is clear (no lock, no failing CI, no backlog). It can be enabled on a per stack basis via the settings page.
400
+
401
+ It can be customized via several `shipit.yml` properties:
402
+
403
+ **<code>merge.revalidate_after</code>** a duration after which pull requests that couldn't be merged are rejected from the queue. Defaults to unlimited.
404
+
405
+ For example:
406
+ ```yml
407
+ merge:
408
+ revalidate_after: 12m30s
409
+ ```
410
+
411
+ **<code>merge.require</code>** contains an array of the [statuses context](https://developer.github.com/v3/repos/statuses/) that you want Shipit to consider as failing if they aren't present on the pull request. Defaults to `ci.require` if present, or empty otherwise.
412
+
413
+ For example:
414
+ ```yml
415
+ merge:
416
+ require:
417
+ - continuous-integration/travis-ci/push
418
+ ```
419
+
420
+ **<code>merge.ignore</code>** contains an array of the [statuses context](https://developer.github.com/v3/repos/statuses/) that you want Shipit not to consider when merging pull requests. Defaults to the union of `ci.allow_failures` and `ci.hide` if any is present or empty otherwise.
421
+
422
+ For example:
423
+ ```yml
424
+ merge:
425
+ ignore:
426
+ - codeclimate
427
+ ```
428
+
396
429
  <h3 id="custom-tasks">Custom tasks</h3>
397
430
 
398
431
  You can create custom tasks that users execute directly from a stack's overview page in Shipit. To create a new custom task, specify its parameters in the `tasks` section of the `shipit.yml` file. For example:
@@ -0,0 +1,63 @@
1
+ class PageUpdater
2
+ DEBOUNCE = 100
3
+ RETRY_DELAY = 5000
4
+ MAX_RETRIES = 5
5
+
6
+ @callbacks: []
7
+ @afterUpdate: (callback) ->
8
+ @callbacks.push(callback)
9
+
10
+ constructor: (@channel, @selectors) ->
11
+ @parser = new DOMParser()
12
+ @source = @listen()
13
+ @previousLastModified = null
14
+
15
+ requestUpdate: =>
16
+ @updateRequested = true
17
+ @scheduleUpdate()
18
+
19
+ scheduleUpdate: =>
20
+ return if @updateScheduled
21
+ return unless @updateRequested
22
+ setTimeout(@fetchPage, DEBOUNCE)
23
+ @updateScheduled = true
24
+
25
+ fetchPage: (message) =>
26
+ @updateRequested = false
27
+ jQuery.get(window.location.toString()).done(@updatePage).fail(=> @updateScheduled = false)
28
+
29
+ updatePage: (html, status, response) =>
30
+ lastModified = response.getResponseHeader('last-modified')
31
+ if lastModified? and lastModified != @previousLastModified
32
+ @previousLastModified = lastModified
33
+
34
+ newDocument = @parser.parseFromString(html, 'text/html')
35
+ for selector in @selectors
36
+ $(selector).html(newDocument.querySelectorAll("#{selector} > *"))
37
+ for callback in PageUpdater.callbacks
38
+ callback()
39
+
40
+ @updateScheduled = false
41
+
42
+ listen: ->
43
+ @source = new EventSource(@channel)
44
+ @source.addEventListener('update', @requestUpdate)
45
+ @retries = MAX_RETRIES
46
+ @interval = setInterval =>
47
+ switch @source.readyState
48
+ when @source.CLOSED
49
+ clearInterval(@interval)
50
+ if @retries > 0
51
+ @retries -= 1
52
+ @listen()
53
+ else
54
+ @retries = MAX_RETRIES
55
+ , RETRY_DELAY
56
+
57
+ jQuery ($) ->
58
+ PageUpdater.afterUpdate -> $('time[data-time-ago]').timeago()
59
+
60
+ channel = $('meta[name=subscription-channel]').attr('content')
61
+ selectors = (e.content for e in $('meta[name=subscription-selector]'))
62
+ if channel and selectors
63
+ new PageUpdater(channel, selectors)
@@ -20,24 +20,12 @@ jQuery ($) ->
20
20
 
21
21
  displayIgnoreCiMessage()
22
22
 
23
- updatePage = (message) ->
24
- payload = JSON.parse(message.data)
25
- $('[data-layout-content]').load("#{payload.url} [data-layout-content] > *", -> $('time[data-time-ago]').timeago())
26
-
27
- retries = 0
28
- listenToEventSource = (url) ->
29
- source = new EventSource(url)
30
- source.addEventListener 'stack.update', updatePage
31
- interval = setInterval ->
32
- switch source.readyState
33
- when source.CLOSED
34
- clearInterval(interval)
35
- if retries > 0
36
- retries -= 1
37
- listenToEventSource(url)
38
- else
39
- retries = 2
40
- , 30000
41
-
42
- $('[data-event-stream]').each ->
43
- listenToEventSource($(this).data('event-stream'))
23
+ $(document).on 'click', '.setting-ccmenu input[type=submit]', (event) ->
24
+ event.preventDefault()
25
+ $(event.target).prop('disabled', true)
26
+ $.get(event.target.dataset.remote).done((data) ->
27
+ $('#ccmenu-url').val(data.ccmenu_url).removeClass('hidden')
28
+ $(event.target).addClass('hidden')
29
+ ).fail(->
30
+ $(event.target).prop('disabled', false)
31
+ )
@@ -105,9 +105,9 @@ p {
105
105
  a {
106
106
  text-decoration: none;
107
107
  cursor: pointer;
108
- color: #248af2;
108
+ color: $blue;
109
109
  &.subdued { color: #999; }
110
- &.subdued:hover { color: #248af2; }
110
+ &.subdued:hover { color: $blue; }
111
111
  }
112
112
 
113
113
  .more {
@@ -2,7 +2,6 @@
2
2
  // COLORS
3
3
  // =============================================================================
4
4
 
5
- $orange: #EDAA4E;
6
5
  $banner-blue: #96A9BA;
7
6
  $banner-red: #EE6A6A;
8
7
  $blue: #248AF2;
@@ -26,6 +26,20 @@ textarea {
26
26
 
27
27
  .field-wrapper {
28
28
  margin-bottom: 1.5rem;
29
+ input:focus {
30
+ border-color: $blue;
31
+ }
32
+ &.inline {
33
+ input {
34
+ &.btn {
35
+ height: 2.5rem;
36
+ }
37
+ display: inline;
38
+ &:focus {
39
+ padding-right: 30px;
40
+ }
41
+ }
42
+ }
29
43
  }
30
44
 
31
45
  .field_with_errors {
@@ -2,7 +2,7 @@
2
2
  // COMMIT LIST
3
3
  // =============================================================================
4
4
 
5
- .commit-list, .task-list {
5
+ .commit-list, .task-list, .pr-list {
6
6
  list-style-type: none;
7
7
  margin: 0; padding: 0;
8
8
  }
@@ -11,7 +11,7 @@
11
11
  float: right;
12
12
  }
13
13
 
14
- .commit, .task {
14
+ .commit, .task, .pr {
15
15
  padding: .75rem 0;
16
16
  display: flex;
17
17
  flex-direction: column;
@@ -70,7 +70,7 @@
70
70
  // COMMIT DETAILS
71
71
  // -----------------------------------------------------------------------------
72
72
 
73
- .commit-details {
73
+ .commit-details, .pr-details {
74
74
  flex-grow: 1;
75
75
 
76
76
  @include media(tablet-down) {
@@ -88,11 +88,17 @@
88
88
  }
89
89
  }
90
90
 
91
- .commit-title a {
92
- color: #333;
91
+ .commit-title, .pr-title {
92
+ a {
93
+ color: #333;
94
+ }
95
+ }
96
+
97
+ .pr-details .pr-number .number {
98
+ color: $blue;
93
99
  }
94
100
 
95
- .commit-meta {
101
+ .commit-meta, .pr-meta {
96
102
  font-size: 0.8em;
97
103
  color: #999;
98
104
  margin: 0;
@@ -100,6 +106,10 @@
100
106
  @include media(desktop) {
101
107
  @include truncate;
102
108
  }
109
+
110
+ .warning {
111
+ color: $orange;
112
+ }
103
113
  }
104
114
 
105
115
  .utc-timecode {
@@ -13,4 +13,12 @@
13
13
  font-size: smaller;
14
14
  font-style: italic;
15
15
  }
16
+
17
+ .ccmenu-url {
18
+ width: 85%;
19
+ }
20
+
21
+ .hidden {
22
+ display: none;
23
+ }
16
24
  }
@@ -154,7 +154,7 @@
154
154
  }
155
155
  }
156
156
  input:focus {
157
- border-color: #248af2;
157
+ border-color: $blue;
158
158
  }
159
159
  }
160
160
 
@@ -25,9 +25,13 @@ module Shipit
25
25
  private
26
26
 
27
27
  def authenticate_api_client
28
- @current_api_client = authenticate_with_http_basic do |*parts|
29
- token = parts.select(&:present?).join('--')
30
- ApiClient.authenticate(token)
28
+ @current_api_client = if Shipit.disable_api_authentication
29
+ UnlimitedApiClient.new
30
+ else
31
+ authenticate_with_http_basic do |*parts|
32
+ token = parts.select(&:present?).join('--')
33
+ ApiClient.authenticate(token)
34
+ end
31
35
  end
32
36
  return if @current_api_client
33
37
  headers['WWW-Authenticate'] = 'Basic realm="Authentication token"'
@@ -0,0 +1,33 @@
1
+ module Shipit
2
+ module Api
3
+ class CCMenuController < BaseController
4
+ require_permission :read, :stack
5
+
6
+ class NoDeploy
7
+ def id
8
+ 0
9
+ end
10
+
11
+ def ended_at
12
+ Time.now.utc
13
+ end
14
+ end
15
+
16
+ def show
17
+ latest_deploy = stack.deploys_and_rollbacks.last || NoDeploy.new
18
+ render 'shipit/ccmenu/project.xml.builder', formats: [:xml], locals: {stack: stack, deploy: latest_deploy}
19
+ end
20
+
21
+ private
22
+
23
+ def stack
24
+ @stack ||= Stack.from_param!(params[:stack_id])
25
+ end
26
+
27
+ def authenticate_api_client
28
+ @current_api_client = ApiClient.authenticate(params[:token])
29
+ super unless @current_api_client
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ module Shipit
2
+ module Api
3
+ class PullRequestsController < BaseController
4
+ require_permission :read, :stack
5
+ require_permission :deploy, :stack, only: %i(update destroy)
6
+
7
+ def index
8
+ render_resources stack.pull_requests.includes(:head).order(id: :desc)
9
+ end
10
+
11
+ def show
12
+ render_resource stack.pull_requests.find_by_number!(params[:id])
13
+ end
14
+
15
+ def update
16
+ pull_request = PullRequest.request_merge!(stack, params[:id], current_user)
17
+ if pull_request.waiting?
18
+ head :accepted
19
+ elsif pull_request.merged?
20
+ render status: :method_not_allowed, json: {
21
+ message: "This pull request was already merged.",
22
+ }
23
+ else
24
+ raise "Pull Request is neither waiting nor merged, this should be impossible"
25
+ end
26
+ end
27
+
28
+ def destroy
29
+ if pull_request = stack.pull_requests.where(number: params[:id]).first
30
+ pull_request.cancel! if pull_request.waiting?
31
+ end
32
+ head :no_content
33
+ end
34
+ end
35
+ end
36
+ end
@@ -15,6 +15,7 @@ module Shipit
15
15
  accepts :branch, String
16
16
  accepts :deploy_url, String
17
17
  accepts :ignore_ci, Boolean
18
+ accepts :merge_queue_enabled, Boolean
18
19
  end
19
20
  def create
20
21
  render_resource Stack.create(params)
@@ -0,0 +1,22 @@
1
+ require 'uri'
2
+
3
+ module Shipit
4
+ class CCMenuUrlController < ShipitController
5
+ def fetch
6
+ uri = URI(api_stack_ccmenu_url(stack_id: stack.to_param))
7
+ uri.query = {'token' => client.authentication_token}.to_query
8
+ render json: {ccmenu_url: uri.to_s}
9
+ end
10
+
11
+ private
12
+
13
+ def client
14
+ @client ||= ApiClient.create_with(permissions: %w(read:stack))
15
+ .find_or_create_by!(creator: current_user, name: 'CCMenu Client')
16
+ end
17
+
18
+ def stack
19
+ @stack ||= Stack.from_param!(params[:stack_id])
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module Shipit
2
+ class PullRequestsController < ShipitController
3
+ def index
4
+ @pull_requests = stack.pull_requests.queued
5
+ end
6
+
7
+ def create
8
+ if pr_number = PullRequest.extract_number(stack, params[:number_or_url])
9
+ pull_request = PullRequest.request_merge!(stack, pr_number, current_user)
10
+ flash[:success] = "Pull request ##{pull_request.number} added to the queue."
11
+ else
12
+ flash[:warning] = "Invalid or missing pull request number."
13
+ end
14
+ redirect_to stack_pull_requests_path
15
+ end
16
+
17
+ def destroy
18
+ pull_request = stack.pull_requests.find(params[:id])
19
+ pull_request.cancel!
20
+ flash[:success] = 'Merge canceled'
21
+ redirect_to stack_pull_requests_path
22
+ end
23
+
24
+ private
25
+
26
+ def stack
27
+ @stack ||= Stack.from_param!(params[:stack_id])
28
+ end
29
+ end
30
+ end
@@ -85,8 +85,13 @@ module Shipit
85
85
  end
86
86
 
87
87
  def update_params
88
- params.require(:stack).permit(:deploy_url, :environment,
89
- :continuous_deployment, :ignore_ci)
88
+ params.require(:stack).permit(
89
+ :deploy_url,
90
+ :environment,
91
+ :continuous_deployment,
92
+ :ignore_ci,
93
+ :merge_queue_enabled,
94
+ )
90
95
  end
91
96
  end
92
97
  end