shipit-engine 0.38.0 → 0.40.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 (323) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -4
  3. data/Rakefile +2 -1
  4. data/app/assets/javascripts/shipit/continuous_delivery_schedule.js.coffee +15 -0
  5. data/app/controllers/concerns/shipit/active_model_serializers_patch.rb +1 -0
  6. data/app/controllers/concerns/shipit/api/cacheable.rb +1 -0
  7. data/app/controllers/concerns/shipit/api/paginable.rb +3 -2
  8. data/app/controllers/concerns/shipit/api/rendering.rb +1 -0
  9. data/app/controllers/concerns/shipit/authentication.rb +1 -0
  10. data/app/controllers/concerns/shipit/pagination.rb +3 -2
  11. data/app/controllers/shipit/api/base_controller.rb +12 -10
  12. data/app/controllers/shipit/api/ccmenu_controller.rb +2 -1
  13. data/app/controllers/shipit/api/commits_controller.rb +2 -3
  14. data/app/controllers/shipit/api/deploys_controller.rb +6 -1
  15. data/app/controllers/shipit/api/hooks_controller.rb +4 -3
  16. data/app/controllers/shipit/api/locks_controller.rb +1 -0
  17. data/app/controllers/shipit/api/merge_requests_controller.rb +6 -5
  18. data/app/controllers/shipit/api/outputs_controller.rb +1 -0
  19. data/app/controllers/shipit/api/release_statuses_controller.rb +2 -1
  20. data/app/controllers/shipit/api/rollbacks_controller.rb +1 -0
  21. data/app/controllers/shipit/api/stacks_controller.rb +15 -14
  22. data/app/controllers/shipit/api/tasks_controller.rb +6 -5
  23. data/app/controllers/shipit/api_clients_controller.rb +6 -7
  24. data/app/controllers/shipit/ccmenu_url_controller.rb +3 -2
  25. data/app/controllers/shipit/commit_checks_controller.rb +2 -1
  26. data/app/controllers/shipit/commits_controller.rb +1 -0
  27. data/app/controllers/shipit/continuous_delivery_schedules_controller.rb +42 -0
  28. data/app/controllers/shipit/deploys_controller.rb +6 -5
  29. data/app/controllers/shipit/github_authentication_controller.rb +6 -0
  30. data/app/controllers/shipit/merge_requests_controller.rb +1 -0
  31. data/app/controllers/shipit/merge_status_controller.rb +30 -26
  32. data/app/controllers/shipit/release_statuses_controller.rb +1 -0
  33. data/app/controllers/shipit/repositories_controller.rb +4 -7
  34. data/app/controllers/shipit/rollbacks_controller.rb +2 -1
  35. data/app/controllers/shipit/shipit_controller.rb +1 -0
  36. data/app/controllers/shipit/stacks_controller.rb +27 -31
  37. data/app/controllers/shipit/status_controller.rb +1 -0
  38. data/app/controllers/shipit/tasks_controller.rb +3 -1
  39. data/app/controllers/shipit/webhooks_controller.rb +2 -1
  40. data/app/helpers/shipit/api_clients_helper.rb +1 -0
  41. data/app/helpers/shipit/chunks_helper.rb +3 -1
  42. data/app/helpers/shipit/deploys_helper.rb +7 -3
  43. data/app/helpers/shipit/github_url_helper.rb +5 -4
  44. data/app/helpers/shipit/merge_status_helper.rb +1 -0
  45. data/app/helpers/shipit/shipit_helper.rb +11 -10
  46. data/app/helpers/shipit/stacks_helper.rb +10 -11
  47. data/app/helpers/shipit/tasks_helper.rb +2 -1
  48. data/app/jobs/shipit/background_job/unique.rb +3 -2
  49. data/app/jobs/shipit/background_job.rb +9 -1
  50. data/app/jobs/shipit/cache_deploy_spec_job.rb +2 -1
  51. data/app/jobs/shipit/chunk_rollup_job.rb +1 -0
  52. data/app/jobs/shipit/clear_git_cache_job.rb +1 -0
  53. data/app/jobs/shipit/continuous_delivery_job.rb +5 -0
  54. data/app/jobs/shipit/create_on_github_job.rb +1 -0
  55. data/app/jobs/shipit/create_release_statuses_job.rb +2 -0
  56. data/app/jobs/shipit/deferred_touch_job.rb +1 -0
  57. data/app/jobs/shipit/deliver_hook_job.rb +1 -0
  58. data/app/jobs/shipit/destroy_job.rb +1 -0
  59. data/app/jobs/shipit/destroy_repository_job.rb +1 -0
  60. data/app/jobs/shipit/destroy_stack_job.rb +36 -15
  61. data/app/jobs/shipit/emit_event_job.rb +1 -0
  62. data/app/jobs/shipit/fetch_commit_stats_job.rb +1 -0
  63. data/app/jobs/shipit/fetch_deployed_revision_job.rb +1 -0
  64. data/app/jobs/shipit/github_sync_job.rb +4 -2
  65. data/app/jobs/shipit/mark_deploy_healthy_job.rb +1 -0
  66. data/app/jobs/shipit/perform_commit_checks_job.rb +1 -0
  67. data/app/jobs/shipit/perform_task_job.rb +1 -0
  68. data/app/jobs/shipit/process_merge_requests_job.rb +2 -0
  69. data/app/jobs/shipit/purge_old_deliveries_job.rb +1 -0
  70. data/app/jobs/shipit/reap_dead_tasks_job.rb +1 -0
  71. data/app/jobs/shipit/refresh_check_runs_job.rb +1 -0
  72. data/app/jobs/shipit/refresh_github_user_job.rb +1 -0
  73. data/app/jobs/shipit/refresh_merge_request_job.rb +1 -0
  74. data/app/jobs/shipit/refresh_statuses_job.rb +1 -0
  75. data/app/jobs/shipit/setup_github_hook_job.rb +1 -0
  76. data/app/jobs/shipit/update_estimated_deploy_duration_job.rb +1 -0
  77. data/app/jobs/shipit/update_github_last_deployed_ref_job.rb +6 -7
  78. data/app/models/concerns/shipit/deferred_touch.rb +5 -2
  79. data/app/models/shipit/anonymous_user.rb +4 -5
  80. data/app/models/shipit/api_client.rb +5 -3
  81. data/app/models/shipit/application_record.rb +1 -0
  82. data/app/models/shipit/check_run.rb +7 -6
  83. data/app/models/shipit/command_line_user.rb +4 -5
  84. data/app/models/shipit/commit.rb +46 -32
  85. data/app/models/shipit/commit_checks.rb +4 -2
  86. data/app/models/shipit/commit_deployment.rb +7 -5
  87. data/app/models/shipit/commit_deployment_status.rb +5 -3
  88. data/app/models/shipit/commit_message.rb +2 -0
  89. data/app/models/shipit/continuous_delivery_schedule.rb +84 -0
  90. data/app/models/shipit/delivery.rb +5 -4
  91. data/app/models/shipit/deploy.rb +46 -26
  92. data/app/models/shipit/deploy_spec/bundler_discovery.rb +3 -1
  93. data/app/models/shipit/deploy_spec/capistrano_discovery.rb +1 -0
  94. data/app/models/shipit/deploy_spec/file_system.rb +48 -17
  95. data/app/models/shipit/deploy_spec/kubernetes_discovery.rb +4 -3
  96. data/app/models/shipit/deploy_spec/lerna_discovery.rb +32 -31
  97. data/app/models/shipit/deploy_spec/npm_discovery.rb +18 -13
  98. data/app/models/shipit/deploy_spec/pypi_discovery.rb +5 -4
  99. data/app/models/shipit/deploy_spec/rubygems_discovery.rb +1 -0
  100. data/app/models/shipit/deploy_spec.rb +25 -30
  101. data/app/models/shipit/deploy_stats.rb +6 -1
  102. data/app/models/shipit/duration.rb +5 -3
  103. data/app/models/shipit/ephemeral_commit_checks.rb +8 -7
  104. data/app/models/shipit/github_hook.rb +1 -0
  105. data/app/models/shipit/github_status.rb +1 -0
  106. data/app/models/shipit/hook.rb +9 -7
  107. data/app/models/shipit/membership.rb +1 -0
  108. data/app/models/shipit/merge_request.rb +26 -16
  109. data/app/models/shipit/output_chunk.rb +1 -0
  110. data/app/models/shipit/provisioning_handler.rb +1 -0
  111. data/app/models/shipit/pull_request.rb +2 -2
  112. data/app/models/shipit/record.rb +1 -0
  113. data/app/models/shipit/release_status.rb +4 -3
  114. data/app/models/shipit/repository.rb +12 -11
  115. data/app/models/shipit/review_stack.rb +3 -1
  116. data/app/models/shipit/review_stack_provisioning_queue.rb +2 -2
  117. data/app/models/shipit/rollback.rb +2 -0
  118. data/app/models/shipit/stack.rb +71 -60
  119. data/app/models/shipit/status/common.rb +1 -0
  120. data/app/models/shipit/status/group.rb +5 -3
  121. data/app/models/shipit/status/missing.rb +2 -1
  122. data/app/models/shipit/status/unknown.rb +1 -0
  123. data/app/models/shipit/status.rb +5 -4
  124. data/app/models/shipit/task.rb +40 -31
  125. data/app/models/shipit/task_definition.rb +10 -7
  126. data/app/models/shipit/task_execution_strategy/default.rb +13 -13
  127. data/app/models/shipit/team.rb +13 -12
  128. data/app/models/shipit/undeployed_commit.rb +8 -3
  129. data/app/models/shipit/unlimited_api_client.rb +2 -2
  130. data/app/models/shipit/user.rb +23 -16
  131. data/app/models/shipit/variable_definition.rb +2 -1
  132. data/app/models/shipit/webhooks/handlers/check_suite_handler.rb +1 -0
  133. data/app/models/shipit/webhooks/handlers/handler.rb +1 -0
  134. data/app/models/shipit/webhooks/handlers/membership_handler.rb +1 -0
  135. data/app/models/shipit/webhooks/handlers/pull_request/assigned_handler.rb +10 -10
  136. data/app/models/shipit/webhooks/handlers/pull_request/closed_handler.rb +1 -1
  137. data/app/models/shipit/webhooks/handlers/pull_request/edited_handler.rb +10 -10
  138. data/app/models/shipit/webhooks/handlers/pull_request/label_capturing_handler.rb +2 -2
  139. data/app/models/shipit/webhooks/handlers/pull_request/labeled_handler.rb +2 -2
  140. data/app/models/shipit/webhooks/handlers/pull_request/reopened_handler.rb +1 -1
  141. data/app/models/shipit/webhooks/handlers/pull_request/review_stack_adapter.rb +3 -3
  142. data/app/models/shipit/webhooks/handlers/pull_request/unlabeled_handler.rb +1 -1
  143. data/app/models/shipit/webhooks/handlers/push_handler.rb +2 -1
  144. data/app/models/shipit/webhooks/handlers/status_handler.rb +1 -0
  145. data/app/models/shipit/webhooks.rb +3 -2
  146. data/app/serializers/concerns/shipit/conditional_attributes.rb +1 -0
  147. data/app/serializers/shipit/anonymous_user_serializer.rb +1 -0
  148. data/app/serializers/shipit/command_line_user_serializer.rb +1 -0
  149. data/app/serializers/shipit/commit_serializer.rb +2 -1
  150. data/app/serializers/shipit/deploy_serializer.rb +1 -0
  151. data/app/serializers/shipit/hook_serializer.rb +1 -0
  152. data/app/serializers/shipit/merge_request_serializer.rb +2 -1
  153. data/app/serializers/shipit/rollback_serializer.rb +1 -0
  154. data/app/serializers/shipit/short_commit_serializer.rb +1 -0
  155. data/app/serializers/shipit/stack_serializer.rb +4 -3
  156. data/app/serializers/shipit/tail_task_serializer.rb +4 -1
  157. data/app/serializers/shipit/task_serializer.rb +1 -0
  158. data/app/serializers/shipit/user_serializer.rb +1 -0
  159. data/app/validators/ascii_only_validator.rb +4 -3
  160. data/app/validators/subset_validator.rb +1 -0
  161. data/app/views/shipit/_variables.html.erb +1 -1
  162. data/app/views/shipit/ccmenu/project.xml.builder +2 -1
  163. data/app/views/shipit/continuous_delivery_schedules/show.html.erb +59 -0
  164. data/app/views/shipit/merge_status/failure.html.erb +1 -1
  165. data/app/views/shipit/missing_settings.html.erb +1 -1
  166. data/app/views/shipit/stacks/_settings_form.erb +1 -0
  167. data/config/initializers/inflections.rb +1 -0
  168. data/config/locales/en.yml +1 -0
  169. data/config/routes.rb +21 -18
  170. data/config/secrets.development.example.yml +1 -1
  171. data/config/secrets.development.shopify.yml +1 -1
  172. data/db/migrate/20240821003007_add_continuous_delivery_schedules.rb +13 -0
  173. data/db/migrate/20250207203053_embiggen_github_ids.rb +8 -0
  174. data/lib/shipit/cast_value.rb +1 -0
  175. data/lib/shipit/command.rb +29 -9
  176. data/lib/shipit/commands.rb +4 -2
  177. data/lib/shipit/csv_serializer.rb +3 -0
  178. data/lib/shipit/deploy_commands.rb +2 -1
  179. data/lib/shipit/engine.rb +6 -5
  180. data/lib/shipit/environment_variables.rb +2 -0
  181. data/lib/shipit/first_parent_commits_iterator.rb +2 -3
  182. data/lib/shipit/flock.rb +11 -9
  183. data/lib/shipit/github_app.rb +14 -16
  184. data/lib/shipit/github_http_cache_middleware.rb +1 -0
  185. data/lib/shipit/null_serializer.rb +1 -0
  186. data/lib/shipit/octokit_check_runs.rb +2 -3
  187. data/lib/shipit/octokit_iterator.rb +2 -0
  188. data/lib/shipit/paginator.rb +1 -0
  189. data/lib/shipit/rollback_commands.rb +2 -1
  190. data/lib/shipit/same_site_cookie_middleware.rb +1 -0
  191. data/lib/shipit/simple_message_verifier.rb +1 -0
  192. data/lib/shipit/stack_commands.rb +35 -27
  193. data/lib/shipit/stat.rb +1 -0
  194. data/lib/shipit/task_commands.rb +7 -6
  195. data/lib/shipit/version.rb +2 -1
  196. data/lib/shipit.rb +30 -17
  197. data/lib/tasks/cron.rake +2 -1
  198. data/lib/tasks/dev.rake +3 -2
  199. data/lib/tasks/shipit.rake +3 -2
  200. data/lib/tasks/teams.rake +3 -2
  201. data/test/controllers/api/base_controller_test.rb +1 -0
  202. data/test/controllers/api/ccmenu_controller_test.rb +4 -3
  203. data/test/controllers/api/commits_controller_test.rb +1 -0
  204. data/test/controllers/api/deploys_controller_test.rb +26 -1
  205. data/test/controllers/api/hooks_controller_test.rb +6 -5
  206. data/test/controllers/api/locks_controller_test.rb +1 -0
  207. data/test/controllers/api/merge_requests_controller_test.rb +1 -0
  208. data/test/controllers/api/outputs_controller_test.rb +1 -0
  209. data/test/controllers/api/release_statuses_controller_test.rb +4 -3
  210. data/test/controllers/api/rollback_controller_test.rb +3 -2
  211. data/test/controllers/api/stacks_controller_test.rb +13 -12
  212. data/test/controllers/api/tasks_controller_test.rb +7 -6
  213. data/test/controllers/api_clients_controller_test.rb +10 -10
  214. data/test/controllers/ccmenu_controller_test.rb +1 -0
  215. data/test/controllers/commit_checks_controller_test.rb +1 -0
  216. data/test/controllers/commits_controller_test.rb +9 -8
  217. data/test/controllers/continuous_delivery_schedules_controller_test.rb +66 -0
  218. data/test/controllers/deploys_controller_test.rb +4 -2
  219. data/test/controllers/github_authentication_controller_test.rb +6 -4
  220. data/test/controllers/merge_requests_controller_test.rb +1 -0
  221. data/test/controllers/merge_status_controller_test.rb +5 -4
  222. data/test/controllers/release_statuses_controller_test.rb +1 -0
  223. data/test/controllers/repositories_controller_test.rb +6 -5
  224. data/test/controllers/rollbacks_controller_test.rb +3 -2
  225. data/test/controllers/stacks_controller_test.rb +8 -6
  226. data/test/controllers/status_controller_test.rb +1 -0
  227. data/test/controllers/tasks_controller_test.rb +13 -5
  228. data/test/controllers/webhooks_controller_test.rb +10 -9
  229. data/test/dummy/config/application.rb +2 -1
  230. data/test/dummy/config/initializers/0_load_development_secrets.rb +2 -2
  231. data/test/dummy/config/secrets.development.json +3 -0
  232. data/test/dummy/config/secrets.test.json +21 -0
  233. data/test/dummy/db/schema.rb +33 -6
  234. data/test/fixtures/shipit/commits.yml +7 -7
  235. data/test/fixtures/shipit/stacks.yml +4 -10
  236. data/test/fixtures/shipit/tasks.yml +3 -3
  237. data/test/helpers/api_helper.rb +2 -3
  238. data/test/helpers/fixture_aliases_helper.rb +1 -0
  239. data/test/helpers/hooks_helper.rb +1 -0
  240. data/test/helpers/json_helper.rb +4 -3
  241. data/test/helpers/links_helper.rb +2 -1
  242. data/test/helpers/payloads_helper.rb +1 -0
  243. data/test/helpers/queries_helper.rb +4 -3
  244. data/test/jobs/cache_deploy_spec_job_test.rb +3 -2
  245. data/test/jobs/chunk_rollup_job_test.rb +3 -2
  246. data/test/jobs/deliver_hook_job_test.rb +1 -0
  247. data/test/jobs/destroy_repository_job_test.rb +1 -0
  248. data/test/jobs/destroy_stack_job_test.rb +12 -0
  249. data/test/jobs/emit_event_job_test.rb +1 -0
  250. data/test/jobs/fetch_commit_stats_job_test.rb +1 -0
  251. data/test/jobs/fetch_deployed_revision_job_test.rb +1 -0
  252. data/test/jobs/github_sync_job_test.rb +22 -21
  253. data/test/jobs/mark_deploy_healthy_job_test.rb +1 -0
  254. data/test/jobs/perform_task_job_test.rb +3 -3
  255. data/test/jobs/process_merge_requests_job_test.rb +7 -6
  256. data/test/jobs/purge_old_deliveries_job_test.rb +1 -0
  257. data/test/jobs/reap_dead_tasks_job_test.rb +1 -0
  258. data/test/jobs/refresh_github_user_job_test.rb +1 -0
  259. data/test/jobs/refresh_status_job_test.rb +1 -0
  260. data/test/jobs/shipit/background_job_test.rb +35 -0
  261. data/test/jobs/shipit/continuous_delivery_job_test.rb +31 -0
  262. data/test/jobs/unique_job_test.rb +3 -1
  263. data/test/jobs/update_github_last_deployed_ref_job_test.rb +1 -0
  264. data/test/middleware/same_site_cookie_middleware_test.rb +2 -2
  265. data/test/models/api_client_test.rb +1 -0
  266. data/test/models/commit_checks_test.rb +2 -1
  267. data/test/models/commit_deployment_status_test.rb +2 -2
  268. data/test/models/commit_deployment_test.rb +4 -3
  269. data/test/models/commits_test.rb +72 -70
  270. data/test/models/delivery_test.rb +3 -2
  271. data/test/models/deploy_spec_test.rb +113 -109
  272. data/test/models/deploy_stats_test.rb +1 -0
  273. data/test/models/deploys_test.rb +65 -56
  274. data/test/models/duration_test.rb +1 -1
  275. data/test/models/github_hook_test.rb +1 -0
  276. data/test/models/hook_test.rb +7 -4
  277. data/test/models/membership_test.rb +1 -0
  278. data/test/models/merge_request_test.rb +26 -20
  279. data/test/models/release_statuses_test.rb +2 -1
  280. data/test/models/rollbacks_test.rb +4 -3
  281. data/test/models/shipit/check_run_test.rb +16 -15
  282. data/test/models/shipit/continuous_delivery_schedule_test.rb +109 -0
  283. data/test/models/shipit/deploy_spec/file_system_test.rb +54 -10
  284. data/test/models/shipit/pull_request_test.rb +9 -9
  285. data/test/models/shipit/repository_test.rb +3 -2
  286. data/test/models/shipit/review_stack_provisioning_queue_test.rb +2 -2
  287. data/test/models/shipit/{stacks_test.rb → stack_test.rb} +48 -34
  288. data/test/models/shipit/webhooks/handlers/pull_request/closed_handler_test.rb +36 -34
  289. data/test/models/shipit/webhooks/handlers/pull_request/label_capturing_handler_test.rb +28 -28
  290. data/test/models/shipit/webhooks/handlers/pull_request/labeled_handler_test.rb +42 -42
  291. data/test/models/shipit/webhooks/handlers/pull_request/opened_handler_test.rb +33 -33
  292. data/test/models/shipit/webhooks/handlers/pull_request/reopened_handler_test.rb +37 -37
  293. data/test/models/shipit/webhooks/handlers/pull_request/review_stack_adapter_test.rb +1 -1
  294. data/test/models/shipit/webhooks/handlers/pull_request/unlabeled_handler_test.rb +44 -42
  295. data/test/models/shipit/webhooks/handlers_test.rb +1 -0
  296. data/test/models/status/group_test.rb +3 -2
  297. data/test/models/status/missing_test.rb +1 -0
  298. data/test/models/status_test.rb +2 -1
  299. data/test/models/task_definitions_test.rb +7 -6
  300. data/test/models/tasks_test.rb +5 -4
  301. data/test/models/team_test.rb +5 -4
  302. data/test/models/undeployed_commits_test.rb +10 -9
  303. data/test/models/users_test.rb +29 -20
  304. data/test/test_command_integration.rb +1 -1
  305. data/test/test_helper.rb +12 -10
  306. data/test/unit/anonymous_user_serializer_test.rb +1 -0
  307. data/test/unit/command_test.rb +10 -1
  308. data/test/unit/commands_test.rb +1 -0
  309. data/test/unit/commit_serializer_test.rb +1 -0
  310. data/test/unit/csv_serializer_test.rb +3 -2
  311. data/test/unit/deploy_commands_test.rb +33 -23
  312. data/test/unit/deploy_serializer_test.rb +1 -0
  313. data/test/unit/environment_variables_test.rb +2 -1
  314. data/test/unit/github_app_test.rb +11 -10
  315. data/test/unit/github_apps_test.rb +19 -18
  316. data/test/unit/github_url_helper_test.rb +1 -0
  317. data/test/unit/line_buffer_test.rb +1 -1
  318. data/test/unit/rollback_commands_test.rb +2 -1
  319. data/test/unit/shipit_helper_test.rb +1 -0
  320. data/test/unit/shipit_test.rb +47 -1
  321. data/test/unit/user_serializer_test.rb +1 -0
  322. data/test/unit/variable_definition_test.rb +4 -3
  323. metadata +61 -47
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class Commit < Record
4
5
  include DeferredTouch
@@ -21,7 +22,7 @@ module Shipit
21
22
  after_create { stack.update_undeployed_commits_count }
22
23
 
23
24
  after_commit :schedule_refresh_statuses!, :schedule_refresh_check_runs!, :schedule_fetch_stats!,
24
- :schedule_continuous_delivery, on: :create
25
+ :schedule_continuous_delivery, on: :create
25
26
 
26
27
  belongs_to :author, class_name: 'User', optional: true, inverse_of: :authored_commits
27
28
  belongs_to :committer, class_name: 'User', optional: true, inverse_of: :commits
@@ -54,25 +55,29 @@ module Shipit
54
55
  scope :reachable, -> { where(detached: false) }
55
56
 
56
57
  delegate :broadcast_update, :github_repo_name, :hidden_statuses, :required_statuses, :blocking_statuses,
57
- :soft_failing_statuses, to: :stack
58
+ :soft_failing_statuses, to: :stack
58
59
 
59
60
  def self.newer_than(commit)
60
61
  return all unless commit
62
+
61
63
  where('id > ?', commit.try(:id) || commit)
62
64
  end
63
65
 
64
66
  def self.older_than(commit)
65
67
  return all unless commit
68
+
66
69
  where('id < ?', commit.try(:id) || commit)
67
70
  end
68
71
 
69
72
  def self.since(commit)
70
73
  return all unless commit
74
+
71
75
  where('id >= ?', commit.try(:id) || commit)
72
76
  end
73
77
 
74
78
  def self.until(commit)
75
79
  return all unless commit
80
+
76
81
  where('id <= ?', commit.try(:id) || commit)
77
82
  end
78
83
 
@@ -85,12 +90,11 @@ module Shipit
85
90
  end
86
91
 
87
92
  def self.by_sha(sha)
88
- if sha.to_s.size < 6
89
- raise AmbiguousRevision, "Short SHA1 #{sha} is ambiguous (too short)"
90
- end
93
+ raise AmbiguousRevision, "Short SHA1 #{sha} is ambiguous (too short)" if sha.to_s.size < 6
91
94
 
92
95
  commits = where('sha like ?', "#{sha}%").take(2)
93
96
  raise AmbiguousRevision, "Short SHA1 #{sha} is ambiguous (matches multiple commits)" if commits.size > 1
97
+
94
98
  commits.first
95
99
  end
96
100
 
@@ -107,26 +111,22 @@ module Shipit
107
111
  record = new(
108
112
  sha: commit.sha,
109
113
  message: commit.commit.message,
110
- author: author,
111
- committer: committer,
114
+ author:,
115
+ committer:,
112
116
  committed_at: commit.commit.committer.date,
113
117
  authored_at: commit.commit.author.date,
114
118
  additions: commit.stats&.additions,
115
- deletions: commit.stats&.deletions,
119
+ deletions: commit.stats&.deletions
116
120
  )
117
121
 
118
- if record.pull_request?
119
- record.pull_request_head_sha = commit.parents.last.sha
120
- end
122
+ record.pull_request_head_sha = commit.parents.last.sha if record.pull_request?
121
123
 
122
124
  record
123
125
  end
124
126
 
125
127
  def message=(message)
126
128
  limit = self.class.columns_hash['message'].limit
127
- if limit && message && message.bytesize > limit
128
- message = message.truncate_bytes(limit)
129
- end
129
+ message = message.truncate_bytes(limit) if limit && message && message.bytesize > limit
130
130
  super(message)
131
131
  end
132
132
 
@@ -168,10 +168,26 @@ module Shipit
168
168
  end
169
169
  end
170
170
 
171
- def refresh_check_runs!
171
+ def paginated_check_runs
172
172
  response = stack.handle_github_redirections do
173
- stack.github_api.check_runs(github_repo_name, sha)
173
+ stack.github_api.check_runs(github_repo_name, sha, per_page: 100)
174
+ end
175
+
176
+ return response if stack.github_api.last_response.rels[:next].nil?
177
+
178
+ loop do
179
+ page = stack.handle_github_redirections do
180
+ stack.github_api.get(stack.github_api.last_response.rels[:next].href)
181
+ end
182
+ response.check_runs.concat(page.check_runs)
183
+ break if stack.github_api.last_response.rels[:next].nil?
174
184
  end
185
+
186
+ response
187
+ end
188
+
189
+ def refresh_check_runs!
190
+ response = paginated_check_runs
175
191
  response.check_runs.each do |check_run|
176
192
  create_or_update_check_run_from_github!(check_run)
177
193
  end
@@ -190,11 +206,11 @@ module Shipit
190
206
 
191
207
  @last_release_status = nil
192
208
  release_statuses.create!(
193
- stack: stack,
194
- user: user,
195
- state: state,
196
- target_url: target_url,
197
- description: description,
209
+ stack:,
210
+ user:,
211
+ state:,
212
+ target_url:,
213
+ description:
198
214
  )
199
215
  end
200
216
 
@@ -223,7 +239,7 @@ module Shipit
223
239
  end
224
240
 
225
241
  def children
226
- self.class.where(stack_id: stack_id).newer_than(self)
242
+ self.class.where(stack_id:).newer_than(self)
227
243
  end
228
244
 
229
245
  def detach_children!
@@ -266,6 +282,7 @@ module Shipit
266
282
 
267
283
  def schedule_continuous_delivery
268
284
  return unless deployable? && stack.continuous_deployment? && stack.deployable?
285
+
269
286
  # This buffer is to allow for statuses and checks to be refreshed before evaluating if the commit is deployable
270
287
  # - e.g. if the commit was fast-forwarded with already passing CI.
271
288
  ContinuousDeliveryJob.set(wait: RECENT_COMMIT_THRESHOLD).perform_later(stack)
@@ -282,7 +299,7 @@ module Shipit
282
299
  def fetch_stats!
283
300
  update!(
284
301
  additions: github_commit.stats&.additions,
285
- deletions: github_commit.stats&.deletions,
302
+ deletions: github_commit.stats&.deletions
286
303
  )
287
304
  end
288
305
 
@@ -300,6 +317,7 @@ module Shipit
300
317
 
301
318
  def identify_merge_request
302
319
  return unless message_parser.pull_request?
320
+
303
321
  if merge_request = stack.merge_requests.find_by(number: message_parser.pull_request_number)
304
322
  self.merge_request = merge_request
305
323
  self.pull_request_number = merge_request.number
@@ -322,14 +340,14 @@ module Shipit
322
340
  def lock(user)
323
341
  update!(
324
342
  locked: true,
325
- lock_author_id: user.id,
343
+ lock_author_id: user.id
326
344
  )
327
345
  end
328
346
 
329
347
  def self.lock_all(user)
330
348
  update_all(
331
349
  locked: true,
332
- lock_author_id: user.id,
350
+ lock_author_id: user.id
333
351
  )
334
352
  end
335
353
 
@@ -356,19 +374,15 @@ module Shipit
356
374
  new_status = status
357
375
 
358
376
  unless already_deployed
359
- payload = { commit: self, stack: stack, status: new_status.state }
360
- if previous_status != new_status
361
- Hook.emit(:commit_status, stack, payload.merge(commit_status: new_status))
362
- end
377
+ payload = { commit: self, stack:, status: new_status.state }
378
+ Hook.emit(:commit_status, stack, payload.merge(commit_status: new_status)) if previous_status != new_status
363
379
  end
364
380
 
365
381
  if previous_status.simple_state != new_status.simple_state
366
382
  if !already_deployed && (!new_status.pending? || previous_status.unknown?)
367
383
  Hook.emit(:deployable_status, stack, payload.merge(deployable_status: new_status))
368
384
  end
369
- if new_status.pending? || new_status.success?
370
- stack.schedule_merges
371
- end
385
+ stack.schedule_merges if new_status.pending? || new_status.success?
372
386
  end
373
387
  new_status
374
388
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class CommitChecks < EphemeralCommitChecks
4
5
  OUTPUT_TTL = 10.minutes.to_i
5
- FINAL_STATUSES = %w(failed error success).freeze
6
+ FINAL_STATUSES = %w[failed error success].freeze
6
7
 
7
8
  def initialize(commit)
8
9
  @commit = commit
@@ -16,12 +17,13 @@ module Shipit
16
17
 
17
18
  def schedule
18
19
  return false if Shipit.redis.get(key('status')).present?
20
+
19
21
  synchronize do
20
22
  return false if Shipit.redis.get(key('status')).present?
21
23
 
22
24
  initialize_redis_state
23
25
  end
24
- PerformCommitChecksJob.perform_later(commit: commit)
26
+ PerformCommitChecksJob.perform_later(commit:)
25
27
  true
26
28
  end
27
29
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class CommitDeployment < Record
4
5
  belongs_to :task
@@ -11,8 +12,8 @@ module Shipit
11
12
  def create_on_github!
12
13
  create_deployment_on_github!
13
14
  statuses.order(id: :asc).each(&:create_on_github!)
14
- rescue Octokit::NotFound, Octokit::Forbidden => error
15
- Rails.logger.warn("Got #{error.class.name} creating deployment or statuses: #{error.message}")
15
+ rescue Octokit::NotFound, Octokit::Forbidden => e
16
+ Rails.logger.warn("Got #{e.class.name} creating deployment or statuses: #{e.message}")
16
17
  # If no one can create the deployment we can only give up
17
18
  end
18
19
 
@@ -23,6 +24,7 @@ module Shipit
23
24
  create_deployment_on_github(stack.github_api)
24
25
  rescue Octokit::ClientError
25
26
  raise if Shipit.github(organization: stack.repository.owner).api == stack.github_api
27
+
26
28
  # If the deploy author didn't gave us the permission to create the deployment we falback the the main shipit
27
29
  # user.
28
30
  #
@@ -55,9 +57,9 @@ module Shipit
55
57
  shipit: {
56
58
  task_id: task.id,
57
59
  from_sha: task.since_commit.sha,
58
- to_sha: task.until_commit.sha,
59
- },
60
- }.to_json,
60
+ to_sha: task.until_commit.sha
61
+ }
62
+ }.to_json
61
63
  )
62
64
  end
63
65
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class CommitDeploymentStatus < Record
4
5
  DESCRIPTION_CHARACTER_LIMIT_ON_GITHUB = 140
@@ -11,10 +12,12 @@ module Shipit
11
12
 
12
13
  def create_on_github!
13
14
  return if github_id?
15
+
14
16
  response = begin
15
17
  create_status_on_github(stack.github_api)
16
18
  rescue Octokit::ClientError
17
19
  raise if Shipit.github(organization: stack.repository.owner).api == stack.github_api
20
+
18
21
  # If the deploy author didn't gave us the permission to create the deployment we falback the the main shipit
19
22
  # user.
20
23
  #
@@ -30,7 +33,7 @@ module Shipit
30
33
  "deployment_description.#{task_type}.#{status}",
31
34
  sha: task.until_commit.short_sha,
32
35
  author: task.author.login,
33
- stack: stack.to_param,
36
+ stack: stack.to_param
34
37
  )
35
38
  end
36
39
 
@@ -48,10 +51,9 @@ module Shipit
48
51
  client.create_deployment_status(
49
52
  commit_deployment.api_url,
50
53
  status,
51
- accept: 'application/vnd.github.flash-preview+json',
52
54
  target_url: url_helpers.stack_deploy_url(stack, task),
53
55
  description: description.truncate(DESCRIPTION_CHARACTER_LIMIT_ON_GITHUB),
54
- environment_url: stack.deploy_url,
56
+ environment_url: stack.deploy_url
55
57
  )
56
58
  end
57
59
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class CommitMessage
4
5
  GITHUB_MERGE_COMMIT_PATTERN = /\AMerge pull request #(?<pr_id>\d+) from \S+\n\n(?<pr_title>.*)/
@@ -27,6 +28,7 @@ module Shipit
27
28
 
28
29
  def parsed
29
30
  return @parsed if defined?(@parsed)
31
+
30
32
  @parsed = to_s.match(GITHUB_MERGE_COMMIT_PATTERN)
31
33
  end
32
34
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shipit
4
+ class ContinuousDeliverySchedule < Record
5
+ belongs_to(:stack)
6
+
7
+ DAYS = %w[sunday monday tuesday wednesday thursday friday saturday].freeze
8
+
9
+ validates(
10
+ *DAYS.map { |day| "#{day}_enabled" },
11
+ inclusion: [true, false]
12
+ )
13
+
14
+ validates(
15
+ *DAYS.product([:start, :end]).map { |parts| parts.join("_") },
16
+ presence: true
17
+ )
18
+
19
+ validate(:validate_time_windows)
20
+
21
+ DeploymentWindow = Struct.new(:starts_at, :ends_at, :enabled) do
22
+ alias_method :enabled?, :enabled
23
+ end
24
+
25
+ def can_deploy?(now = Time.current)
26
+ # Make sure time is in the default time zone so weekdays match what is
27
+ # stored in the database.
28
+ now = now.in_time_zone(Time.zone)
29
+
30
+ deployment_window = get_deployment_window(now.to_date)
31
+
32
+ deployment_window.enabled? &&
33
+ now >= deployment_window.starts_at &&
34
+ now <= deployment_window.ends_at
35
+ end
36
+
37
+ def get_deployment_window(date)
38
+ wday_name = DAYS.fetch(date.wday)
39
+
40
+ enabled = read_attribute("#{wday_name}_enabled")
41
+
42
+ starts_at, ends_at = [:start, :end].map do |bound|
43
+ raw_time = read_attribute("#{wday_name}_#{bound}")
44
+
45
+ # `ActiveRecord::Type::Time` attributes are stored as timestamps
46
+ # normalized to 2000-01-01 so they can't be used for comparisons without
47
+ # having their dates adjusted.
48
+ # https://github.com/rails/rails/blob/ec667e5f114df58087493096253541f1034815af/activemodel/lib/active_model/type/time.rb#L23
49
+ Time.zone.local(
50
+ date.year,
51
+ date.month,
52
+ date.day,
53
+ raw_time.hour,
54
+ raw_time.min
55
+ )
56
+ end
57
+
58
+ DeploymentWindow.new(
59
+ starts_at,
60
+ # Includes the full minute in the configured range. This is required so
61
+ # that a window configured to end at 17:59 actually ends at 17:59:59
62
+ # instead of 17:59:00.
63
+ ends_at.at_end_of_minute,
64
+ enabled
65
+ )
66
+ end
67
+
68
+ private
69
+
70
+ # Make sure every `*_end` attribute comes after its matching `*_start`
71
+ # attribute
72
+ def validate_time_windows
73
+ DAYS.each do |day|
74
+ day_start, day_end = [:start, :end].map { |bound| read_attribute("#{day}_#{bound}") }
75
+
76
+ next unless day_start && day_end
77
+
78
+ next if day_start <= day_end
79
+
80
+ errors.add("#{day}_end", :must_be_after_start, start: day_start.strftime("%I:%M %p"))
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class Delivery < Record
4
- STATUSES = %w(pending scheduled sent).freeze
5
- enum status: STATUSES.zip(STATUSES).to_h
5
+ STATUSES = %w[pending scheduled sent].freeze
6
+ enum :status, STATUSES.zip(STATUSES).to_h
6
7
 
7
8
  belongs_to :hook
8
9
 
9
10
  validates :url, presence: true, url: { no_local: true, allow_blank: true }
10
11
  validates :content_type, presence: true
11
12
 
12
- serialize :response_headers, SafeJSON
13
+ serialize :response_headers, coder: SafeJSON
13
14
 
14
15
  after_commit :purge_old_deliveries, on: :create
15
16
 
@@ -47,7 +48,7 @@ module Shipit
47
48
  'Content-Type' => content_type,
48
49
  'X-Shipit-Event' => event,
49
50
  'X-Shipit-Delivery' => id.to_s,
50
- 'Accept' => '*/*',
51
+ 'Accept' => '*/*'
51
52
  }
52
53
  end
53
54
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'fileutils'
3
4
 
4
5
  module Shipit
@@ -26,7 +27,7 @@ module Shipit
26
27
  'success' => 'success',
27
28
  'faulty' => 'error',
28
29
  'error' => 'error',
29
- 'aborted' => 'error',
30
+ 'aborted' => 'error'
30
31
  }.freeze
31
32
 
32
33
  def append_status(task_status)
@@ -37,7 +38,7 @@ module Shipit
37
38
  "Creating #{github_status} deploy status for deployment #{deployment.id}. "\
38
39
  "Commit: #{deployment.sha}, Github id: #{deployment.github_id}, "\
39
40
  "Repo: #{deployment.stack.repo_name}, Environment: #{deployment.stack.environment}, "\
40
- "API Url: #{deployment.api_url}.",
41
+ "API Url: #{deployment.api_url}."
41
42
  )
42
43
  deployment.statuses.create!(status: github_status)
43
44
  end
@@ -47,7 +48,7 @@ module Shipit
47
48
  "No GitHub status for task status #{task_status}. "\
48
49
  "Commit: #{deployment.sha}, Github id: #{deployment.github_id}, "\
49
50
  "Repo: #{deployment.stack.repo_name}, Environment: #{deployment.stack.environment}, "\
50
- "API Url: #{deployment.api_url}.",
51
+ "API Url: #{deployment.api_url}."
51
52
  )
52
53
  end
53
54
  end
@@ -64,70 +65,75 @@ module Shipit
64
65
 
65
66
  def self.newer_than(deploy)
66
67
  return all unless deploy
68
+
67
69
  where('id > ?', deploy.try(:id) || deploy)
68
70
  end
69
71
 
70
72
  def self.older_than(deploy)
71
73
  return all unless deploy
74
+
72
75
  where('id < ?', deploy.try(:id) || deploy)
73
76
  end
74
77
 
75
78
  def self.since(deploy)
76
79
  return all unless deploy
80
+
77
81
  where('id >= ?', deploy.try(:id) || deploy)
78
82
  end
79
83
 
80
84
  def self.until(deploy)
81
85
  return all unless deploy
86
+
82
87
  where('id <= ?', deploy.try(:id) || deploy)
83
88
  end
84
89
 
85
90
  def build_rollback(user = nil, env: nil, force: false)
86
91
  Rollback.new(
87
92
  user_id: user&.id,
88
- stack_id: stack_id,
93
+ stack_id:,
89
94
  parent_id: id,
90
95
  since_commit: stack.last_deployed_commit,
91
- until_commit: until_commit,
92
- env: env&.to_h || {},
96
+ until_commit:,
97
+ env: env.to_h,
93
98
  allow_concurrency: force,
94
99
  ignored_safeties: force,
95
- max_retries: stack.retries_on_rollback,
100
+ max_retries: stack.retries_on_rollback
96
101
  )
97
102
  end
98
103
 
99
104
  # Rolls the stack back to this deploy
100
105
  def trigger_rollback(user = AnonymousUser.new, env: nil, force: false, lock: true)
101
- rollback = build_rollback(user, env: env, force: force)
106
+ rollback = build_rollback(user, env:, force:)
102
107
  rollback.save!
103
108
  rollback.enqueue
104
109
 
105
110
  if lock
106
111
  lock_reason = "A rollback for #{rollback.since_commit.sha} has been triggered. " \
107
112
  "Please make sure the reason for the rollback has been addressed before deploying again."
108
- stack.update!(lock_reason: lock_reason, lock_author_id: user.id)
113
+ stack.update!(lock_reason:, lock_author_id: user.id)
109
114
  end
110
115
 
111
116
  rollback
112
117
  end
113
118
 
114
119
  # Rolls the stack back to the most recent **previous** successful deploy
115
- def trigger_revert(force: false, rollback_to: nil)
120
+ def trigger_revert(force: false, rollback_to: nil, env: nil)
116
121
  previous_successful_commit = rollback_to&.until_commit || commit_to_rollback_to
117
122
 
118
123
  rollback = Rollback.create!(
119
- user_id: user_id,
120
- stack_id: stack_id,
124
+ user_id:,
125
+ stack_id:,
121
126
  parent_id: id,
122
127
  since_commit: until_commit,
123
128
  until_commit: previous_successful_commit,
124
- allow_concurrency: force,
129
+ env: env || self.env,
130
+ allow_concurrency: force
125
131
  )
126
132
 
127
133
  rollback.enqueue
128
134
  lock_reason = "A rollback for #{until_commit.sha} has been triggered. " \
129
135
  "Please make sure the reason for the rollback has been addressed before deploying again."
130
- stack.update!(lock_reason: lock_reason, lock_author_id: user_id)
136
+ stack.update!(lock_reason:, lock_author_id: user_id)
131
137
  stack.emit_lock_hooks
132
138
  rollback
133
139
  end
@@ -176,6 +182,7 @@ module Shipit
176
182
 
177
183
  def reject!
178
184
  return if failed? || aborted?
185
+
179
186
  transaction do
180
187
  flap! unless flapping?
181
188
  update!(confirmations: [confirmations - 1, -1].min)
@@ -185,6 +192,7 @@ module Shipit
185
192
 
186
193
  def accept!
187
194
  return if success?
195
+
188
196
  transaction do
189
197
  flap! unless flapping?
190
198
  update!(confirmations: [confirmations + 1, 1].max)
@@ -198,13 +206,12 @@ module Shipit
198
206
 
199
207
  delegate :last_release_status, to: :until_commit
200
208
  def append_release_status(state, description, user: self.user)
201
- status = until_commit.create_release_status!(
209
+ until_commit.create_release_status!(
202
210
  state,
203
211
  user: user.presence,
204
212
  target_url: permalink,
205
- description: description,
213
+ description:
206
214
  )
207
- status
208
215
  end
209
216
 
210
217
  def permalink
@@ -225,7 +232,7 @@ module Shipit
225
232
  append_release_status(
226
233
  'success',
227
234
  description,
228
- user: user,
235
+ user:
229
236
  )
230
237
  end
231
238
  end
@@ -236,7 +243,7 @@ module Shipit
236
243
  append_release_status(
237
244
  'failure',
238
245
  description,
239
- user: user,
246
+ user:
240
247
  )
241
248
  end
242
249
  end
@@ -254,6 +261,7 @@ module Shipit
254
261
  # Create one for each pull request in the batch, to give feedback on the PR timeline
255
262
  commits.select(&:pull_request?).each do |commit|
256
263
  next if commit.pull_request_head_sha.blank? # This attribute was not always populated
264
+
257
265
  commit_deployments.create!(sha: commit.pull_request_head_sha)
258
266
  end
259
267
 
@@ -272,13 +280,17 @@ module Shipit
272
280
  when 'aborted', 'aborting'
273
281
  append_release_status('failure', "The deploy on #{stack.environment} was canceled")
274
282
  when 'validating'
275
- append_release_status(
276
- 'pending',
277
- "The deploy on #{stack.environment} succeeded"
278
- ) unless stack.release_status_delay.zero?
283
+ unless stack.release_status_delay.zero?
284
+ append_release_status(
285
+ 'pending',
286
+ "The deploy on #{stack.environment} succeeded"
287
+ )
288
+ end
279
289
 
280
- MarkDeployHealthyJob.set(wait: stack.release_status_delay)
281
- .perform_later(self) if stack.release_status_delay.positive?
290
+ if stack.release_status_delay.positive?
291
+ MarkDeployHealthyJob.set(wait: stack.release_status_delay)
292
+ .perform_later(self)
293
+ end
282
294
  when 'success'
283
295
  if stack.release_status_delay.zero?
284
296
  append_release_status('success', "The deploy on #{stack.environment} succeeded")
@@ -289,11 +301,17 @@ module Shipit
289
301
  def trigger_revert_if_required
290
302
  return unless rollback_once_aborted?
291
303
  return unless supports_rollback?
292
- trigger_revert(rollback_to: rollback_once_aborted_to)
304
+
305
+ if rollback_once_aborted_to
306
+ rollback_once_aborted_to.trigger_rollback(aborted_by, env:, force: true)
307
+ else
308
+ trigger_revert(force: true, env:)
309
+ end
293
310
  end
294
311
 
295
312
  def default_since_commit_id
296
313
  return unless stack
314
+
297
315
  @default_since_commit_id ||= stack.last_completed_deploy&.until_commit_id
298
316
  end
299
317
 
@@ -308,6 +326,7 @@ module Shipit
308
326
 
309
327
  def schedule_continuous_delivery
310
328
  return unless stack.continuous_deployment?
329
+
311
330
  ContinuousDeliveryJob.perform_later(stack)
312
331
  end
313
332
 
@@ -321,6 +340,7 @@ module Shipit
321
340
 
322
341
  def update_latest_deployed_ref
323
342
  return unless previous_changes.include?(:status)
343
+
324
344
  stack.update_latest_deployed_ref if previous_changes[:status].last == 'success'
325
345
  end
326
346
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Shipit
3
4
  class DeploySpec
4
5
  module BundlerDiscovery
5
- DEFAULT_BUNDLER_WITHOUT = %w(default production development test staging benchmark debug).freeze
6
+ DEFAULT_BUNDLER_WITHOUT = %w[default production development test staging benchmark debug].freeze
6
7
 
7
8
  def discover_dependencies_steps
8
9
  discover_bundler || super
@@ -43,6 +44,7 @@ module Shipit
43
44
  def frozen_flag
44
45
  return unless gemfile_lock_exists?
45
46
  return if config('dependencies', 'bundler', 'frozen') == false
47
+
46
48
  '--frozen'
47
49
  end
48
50