shipit-engine 0.33.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -2
  3. data/app/assets/stylesheets/_pages/_deploy.scss +0 -2
  4. data/app/controllers/shipit/api/ccmenu_controller.rb +1 -1
  5. data/app/controllers/shipit/api/deploys_controller.rb +2 -0
  6. data/app/controllers/shipit/api/rollbacks_controller.rb +2 -1
  7. data/app/controllers/shipit/api/stacks_controller.rb +1 -0
  8. data/app/controllers/shipit/deploys_controller.rb +1 -1
  9. data/app/controllers/shipit/stacks_controller.rb +2 -2
  10. data/app/controllers/shipit/tasks_controller.rb +2 -2
  11. data/app/controllers/shipit/webhooks_controller.rb +23 -4
  12. data/app/helpers/shipit/shipit_helper.rb +0 -1
  13. data/app/jobs/shipit/deliver_hook_job.rb +1 -1
  14. data/app/jobs/shipit/github_sync_job.rb +13 -9
  15. data/app/jobs/shipit/update_github_last_deployed_ref_job.rb +1 -1
  16. data/app/models/shipit/anonymous_user.rb +6 -2
  17. data/app/models/shipit/check_run.rb +36 -0
  18. data/app/models/shipit/commit.rb +20 -9
  19. data/app/models/shipit/commit_checks.rb +13 -13
  20. data/app/models/shipit/commit_deployment.rb +3 -3
  21. data/app/models/shipit/commit_deployment_status.rb +3 -3
  22. data/app/models/shipit/deploy.rb +16 -11
  23. data/app/models/shipit/deploy_spec/lerna_discovery.rb +12 -4
  24. data/app/models/shipit/duration.rb +2 -0
  25. data/app/models/shipit/hook.rb +26 -2
  26. data/app/models/shipit/merge_request.rb +9 -7
  27. data/app/models/shipit/pull_request.rb +1 -1
  28. data/app/models/shipit/release_status.rb +1 -1
  29. data/app/models/shipit/repository.rb +9 -3
  30. data/app/models/shipit/review_stack.rb +16 -2
  31. data/app/models/shipit/stack.rb +59 -25
  32. data/app/models/shipit/status/group.rb +1 -1
  33. data/app/models/shipit/task.rb +6 -2
  34. data/app/models/shipit/task_execution_strategy/default.rb +4 -5
  35. data/app/models/shipit/team.rb +4 -2
  36. data/app/models/shipit/user.rb +4 -0
  37. data/app/models/shipit/webhooks/handlers/pull_request/review_stack_adapter.rb +1 -1
  38. data/app/models/shipit/webhooks/handlers/push_handler.rb +4 -1
  39. data/app/serializers/shipit/merge_request_serializer.rb +1 -1
  40. data/app/validators/subset_validator.rb +1 -1
  41. data/app/views/layouts/merge_status.html.erb +1 -1
  42. data/app/views/shipit/stacks/_banners.html.erb +2 -1
  43. data/app/views/shipit/stacks/new.html.erb +1 -1
  44. data/config/secrets.development.example.yml +24 -0
  45. data/config/secrets.development.shopify.yml +20 -9
  46. data/db/migrate/20210325194053_remove_stacks_branch_default.rb +5 -0
  47. data/db/migrate/20210504200438_add_github_updated_at_to_check_runs.rb +5 -0
  48. data/lib/shipit.rb +39 -15
  49. data/lib/shipit/command.rb +7 -6
  50. data/lib/shipit/commands.rb +9 -2
  51. data/lib/shipit/engine.rb +2 -0
  52. data/lib/shipit/flock.rb +8 -1
  53. data/lib/shipit/github_app.rb +7 -5
  54. data/lib/shipit/octokit_iterator.rb +3 -3
  55. data/lib/shipit/simple_message_verifier.rb +2 -2
  56. data/lib/shipit/stack_commands.rb +28 -4
  57. data/lib/shipit/task_commands.rb +6 -0
  58. data/lib/shipit/version.rb +1 -1
  59. data/lib/snippets/publish-lerna-independent-packages +35 -34
  60. data/lib/snippets/publish-lerna-independent-packages-legacy +39 -0
  61. data/test/controllers/api/ccmenu_controller_test.rb +1 -1
  62. data/test/controllers/api/deploys_controller_test.rb +17 -0
  63. data/test/controllers/api/stacks_controller_test.rb +21 -7
  64. data/test/controllers/webhooks_controller_test.rb +26 -11
  65. data/test/dummy/app/assets/config/manifest.js +3 -0
  66. data/test/dummy/config/application.rb +1 -1
  67. data/test/dummy/config/database.yml +9 -0
  68. data/test/dummy/config/environments/development.rb +1 -1
  69. data/test/dummy/config/secrets_double_github_app.yml +79 -0
  70. data/test/dummy/db/schema.rb +5 -4
  71. data/test/dummy/db/seeds.rb +1 -0
  72. data/test/fixtures/payloads/check_suite_master.json +2 -30
  73. data/test/fixtures/payloads/push_master.json +1 -1
  74. data/test/fixtures/payloads/push_not_master.json +1 -1
  75. data/test/fixtures/shipit/commits.yml +2 -2
  76. data/test/fixtures/shipit/hooks.yml +1 -0
  77. data/test/fixtures/shipit/tasks.yml +1 -1
  78. data/test/helpers/json_helper.rb +5 -1
  79. data/test/jobs/github_sync_job_test.rb +2 -1
  80. data/test/models/commit_deployment_status_test.rb +3 -3
  81. data/test/models/commits_test.rb +2 -0
  82. data/test/models/deploy_spec_test.rb +7 -0
  83. data/test/models/deploys_test.rb +18 -0
  84. data/test/models/hook_test.rb +30 -1
  85. data/test/models/merge_request_test.rb +19 -4
  86. data/test/models/shipit/check_run_test.rb +124 -5
  87. data/test/models/shipit/review_stack_test.rb +38 -6
  88. data/test/models/shipit/stacks_test.rb +42 -4
  89. data/test/models/shipit/webhooks/handlers/pull_request/review_stack_adapter_test.rb +24 -0
  90. data/test/models/tasks_test.rb +22 -0
  91. data/test/test_helper.rb +15 -0
  92. data/test/unit/anonymous_user_serializer_test.rb +1 -1
  93. data/test/unit/command_test.rb +5 -0
  94. data/test/unit/commit_serializer_test.rb +1 -1
  95. data/test/unit/deploy_commands_test.rb +70 -14
  96. data/test/unit/deploy_serializer_test.rb +1 -1
  97. data/test/unit/github_app_test.rb +2 -3
  98. data/test/unit/github_apps_test.rb +416 -0
  99. data/test/unit/shipit_deployment_checks_test.rb +77 -0
  100. data/test/unit/shipit_test.rb +14 -0
  101. data/test/unit/user_serializer_test.rb +1 -1
  102. metadata +202 -191
@@ -31,7 +31,7 @@ module Shipit
31
31
  end
32
32
 
33
33
  delegate :pending?, :success?, :error?, :failure?, :unknown?, :missing?, :state, :simple_state,
34
- to: :significant_status
34
+ to: :significant_status
35
35
  delegate :each, :size, :map, to: :statuses
36
36
  delegate :required_statuses, to: :commit
37
37
 
@@ -39,7 +39,7 @@ module Shipit
39
39
  scope :last_seven_days, -> { where("created_at > ?", 7.days.ago) }
40
40
  scope :previous_seven_days, -> { where(created_at: 14.days.ago..7.days.ago) }
41
41
 
42
- scope :due_for_rollup, -> { completed.where(rolled_up: false).where('created_at <= ?', 1.hour.ago) }
42
+ scope :due_for_rollup, -> { not_active.where(rolled_up: false).where('created_at <= ?', 1.hour.ago) }
43
43
 
44
44
  after_save :record_status_change
45
45
  after_create :prevent_concurrency, unless: :allow_concurrency?
@@ -175,7 +175,7 @@ module Shipit
175
175
  end
176
176
 
177
177
  delegate :acquire_git_cache_lock, :async_refresh_deployed_revision, :async_update_estimated_deploy_duration,
178
- to: :stack
178
+ to: :stack
179
179
 
180
180
  delegate :checklist, to: :definition
181
181
 
@@ -283,6 +283,10 @@ module Shipit
283
283
  user || AnonymousUser.new
284
284
  end
285
285
 
286
+ def author=(user)
287
+ super(user.presence)
288
+ end
289
+
286
290
  def finished?
287
291
  !pending? && !running? && !aborting?
288
292
  end
@@ -83,19 +83,18 @@ module Shipit
83
83
  end
84
84
 
85
85
  def capture!(command)
86
- started_at = Time.now
86
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
87
  command.start do
88
88
  @task.ping
89
89
  check_for_abort
90
90
  end
91
- @task.write("$ #{command}\npid: #{command.pid}\nstarted at: #{started_at}\n")
91
+ @task.write("\n$ #{command}\npid: #{command.pid}\n")
92
92
  @task.pid = command.pid
93
93
  command.stream! do |line|
94
94
  @task.write(line)
95
95
  end
96
- @task.write("\n")
97
- finished_at = Time.now
98
- @task.write("pid: #{command.pid}\nfinished at: #{finished_at}\nran in: #{finished_at - started_at} seconds\n")
96
+ finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
97
+ @task.write("pid: #{command.pid} finished in: #{finished_at - started_at} seconds\n")
99
98
  command.success?
100
99
  end
101
100
 
@@ -26,7 +26,8 @@ module Shipit
26
26
  end
27
27
 
28
28
  def find_team_on_github(organization, slug)
29
- teams = Shipit::OctokitIterator.new { Shipit.github.api.org_teams(organization, per_page: 100) }
29
+ gh_api = Shipit.github(organization: organization).api
30
+ teams = Shipit::OctokitIterator.new(github_api: gh_api) { gh_api.org_teams(organization, per_page: 100) }
30
31
  teams.find { |t| t.slug == slug }
31
32
  rescue Octokit::NotFound
32
33
  end
@@ -41,7 +42,8 @@ module Shipit
41
42
  end
42
43
 
43
44
  def refresh_members!
44
- github_members = Shipit::OctokitIterator.new(Shipit.github.api.get(api_url).rels[:members])
45
+ github_api = Shipit.github(organization: organization).api
46
+ github_members = Shipit::OctokitIterator.new(github_api.get(api_url).rels[:members])
45
47
  members = github_members.map { |u| User.find_or_create_from_github(u) }
46
48
  self.members = members
47
49
  save!
@@ -15,6 +15,8 @@ module Shipit
15
15
 
16
16
  def self.find_or_create_by_login!(login)
17
17
  find_or_create_by!(login: login) do |user|
18
+ # Users are global, any app can be used
19
+ # This will not work for users that only exist in an Enterprise install
18
20
  user.github_user = Shipit.github.api.user(login)
19
21
  end
20
22
  end
@@ -91,6 +93,8 @@ module Shipit
91
93
  end
92
94
 
93
95
  def refresh_from_github!
96
+ # Users are global, any app can be used
97
+ # This will not work for users that only exist in an Enterprise install
94
98
  update!(github_user: Shipit.github.api.user(github_id))
95
99
  rescue Octokit::NotFound
96
100
  identify_renamed_user!
@@ -44,8 +44,8 @@ module Shipit
44
44
  return unless stack.archived?
45
45
 
46
46
  stack.transaction do
47
- stack.unarchive!(*args, &block)
48
47
  Shipit::ReviewStackProvisioningQueue.add(stack)
48
+ stack.unarchive!(*args, &block)
49
49
  end
50
50
  end
51
51
 
@@ -7,7 +7,10 @@ module Shipit
7
7
  requires :ref
8
8
  end
9
9
  def process
10
- stacks.where(branch: branch).each(&:sync_github)
10
+ stacks
11
+ .not_archived
12
+ .where(branch: branch)
13
+ .find_each(&:sync_github)
11
14
  end
12
15
 
13
16
  private
@@ -8,7 +8,7 @@ module Shipit
8
8
  has_one :head, serializer: ShortCommitSerializer
9
9
 
10
10
  attributes :id, :number, :title, :github_id, :additions, :deletions, :state, :merge_status, :mergeable,
11
- :merge_requested_at, :rejection_reason, :html_url, :branch, :base_ref
11
+ :merge_requested_at, :rejection_reason, :html_url, :branch, :base_ref
12
12
 
13
13
  def html_url
14
14
  github_pull_request_url(object)
@@ -3,6 +3,6 @@ class SubsetValidator < ActiveModel::EachValidator
3
3
  def validate_each(record, attribute, value)
4
4
  superset = options[:of]
5
5
  rest = value - superset
6
- record.errors.add(attribute, :subset, options) unless rest.empty?
6
+ record.errors.add(attribute, :subset, **options) unless rest.empty?
7
7
  end
8
8
  end
@@ -4,7 +4,7 @@
4
4
  <%= stylesheet_link_tag *(params[:stylesheets] || []).select { |url| url.start_with?('https://assets-cdn.github.com/', 'https://github.githubassets.com/') } %>
5
5
  <%= stylesheet_link_tag 'merge_status' %>
6
6
  </head>
7
- <body>
7
+ <body data-color-mode="<%= params[:mode] %>">
8
8
  <div class="merge-status-container" data-layout-content><%= yield %></div>
9
9
  <%= javascript_include_tag 'merge_status' %>
10
10
  </body>
@@ -69,7 +69,8 @@
69
69
  <h2 class="banner__title">Continuous Delivery Delayed!</h2>
70
70
  <p class="banner__text">
71
71
  Continuous Delivery for this stack is currently paused because
72
- <%= link_to 'the pre-deploy checks failed', stack_commit_checks_path(stack, sha: stack.next_commit_to_deploy.sha) %>.
72
+
73
+ <%= link_to_if stack.deployment_checks_passed?, 'the pre-deploy checks failed', stack_commit_checks_path(stack, sha: stack.next_commit_to_deploy.sha) %>.
73
74
  You can either wait for them to pass, or trigger a deploy manually.
74
75
  </p>
75
76
  </div>
@@ -17,7 +17,7 @@
17
17
  </div>
18
18
  <div class="field-wrapper">
19
19
  <%= f.label :branch %>
20
- <%= f.text_field :branch, placeholder: 'master' %>
20
+ <%= f.text_field :branch, placeholder: '<default branch>' %>
21
21
  </div>
22
22
  <div class="field-wrapper">
23
23
  <%= f.label :environment %>
@@ -1,8 +1,10 @@
1
1
  host: 'localhost:3000'
2
2
  redis_url: 'redis://127.0.0.1:6379/0'
3
3
 
4
+ # For creating an app see: https://github.com/Shopify/shipit-engine/blob/master/docs/setup.md#creating-the-github-app
4
5
  # Can be obtained there: https://github.com/settings/apps
5
6
  # Set the "Authorization callback URL" as `<host>/github/auth/github/callback`
7
+
6
8
  github:
7
9
  app_id:
8
10
  installation_id:
@@ -12,3 +14,25 @@ github:
12
14
  id:
13
15
  secret:
14
16
  teams: # Optional
17
+
18
+ # Use this configuration schema if you are configuring multiple Github applications for different Github organizations
19
+
20
+ # github:
21
+ # somegithuborg:
22
+ # app_id:
23
+ # installation_id:
24
+ # webhook_secret: # nil
25
+ # private_key:
26
+ # oauth:
27
+ # id:
28
+ # secret:
29
+ # teams: # Optional
30
+ # someothergithuborg:
31
+ # app_id:
32
+ # installation_id:
33
+ # webhook_secret: # nil
34
+ # private_key:
35
+ # oauth:
36
+ # id:
37
+ # secret:
38
+ # teams: # Optional
@@ -1,13 +1,24 @@
1
1
  host: 'shipit-engine.myshopify.io'
2
2
  redis_url: 'redis://shipit-engine.railgun:6379'
3
3
 
4
- # TODO: document creating a dev app
4
+ # For creating an app see: https://github.com/Shopify/shipit-engine/blob/master/docs/setup.md#creating-the-github-app
5
+
5
6
  github:
6
- app_id:
7
- installation_id:
8
- webhook_secret: # nil
9
- private_key:
10
- oauth:
11
- id:
12
- secret:
13
- teams:
7
+ somegithuborg:
8
+ app_id:
9
+ installation_id:
10
+ webhook_secret: # nil
11
+ private_key:
12
+ oauth:
13
+ id:
14
+ secret:
15
+ teams:
16
+ someothergithuborg:
17
+ app_id:
18
+ installation_id:
19
+ webhook_secret: # nil
20
+ private_key:
21
+ oauth:
22
+ id:
23
+ secret:
24
+ teams:
@@ -0,0 +1,5 @@
1
+ class RemoveStacksBranchDefault < ActiveRecord::Migration[6.1]
2
+ def change
3
+ change_column_default(:stacks, :branch, from: 'master', to: nil)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class AddGithubUpdatedAtToCheckRuns < ActiveRecord::Migration[6.1]
2
+ def change
3
+ add_column :check_runs, :github_updated_at, :datetime, default: 0
4
+ end
5
+ end
data/lib/shipit.rb CHANGED
@@ -24,7 +24,6 @@ require 'safe_yaml/load'
24
24
  require 'securecompare'
25
25
 
26
26
  require 'redis-objects'
27
- require 'redis-namespace'
28
27
 
29
28
  require 'octokit'
30
29
  require 'faraday-http-cache'
@@ -60,9 +59,12 @@ SafeYAML::OPTIONS[:deserialize_symbols] = false
60
59
  module Shipit
61
60
  extend self
62
61
 
62
+ GithubOrganizationUnknown = Class.new(StandardError)
63
+ TOP_LEVEL_GH_KEYS = [:app_id, :installation_id, :webhook_secret, :private_key, :oauth, :domain]
64
+
63
65
  delegate :table_name_prefix, to: :secrets
64
66
 
65
- attr_accessor :disable_api_authentication, :timeout_exit_codes
67
+ attr_accessor :disable_api_authentication, :timeout_exit_codes, :deployment_checks
66
68
  attr_writer(
67
69
  :internal_hook_receivers,
68
70
  :preferred_org_emails,
@@ -92,7 +94,7 @@ module Shipit
92
94
  secrets.redis_url.present? ? URI(secrets.redis_url) : nil
93
95
  end
94
96
 
95
- def redis(namespace = nil)
97
+ def redis
96
98
  @redis ||= Redis.new(
97
99
  url: redis_url.to_s.presence,
98
100
  logger: Rails.logger,
@@ -100,17 +102,41 @@ module Shipit
100
102
  reconnect_delay: 0.5,
101
103
  reconnect_delay_max: 1,
102
104
  )
103
- return @redis unless namespace
104
- Redis::Namespace.new(namespace, redis: @redis)
105
105
  end
106
106
 
107
- def github
108
- @github ||= GitHubApp.new(secrets.github)
107
+ def github(organization: github_default_organization)
108
+ # Backward compatibility
109
+ # nil signifies the single github app config schema is being used
110
+ if github_default_organization.nil?
111
+ config = secrets.github
112
+ else
113
+ config = github_app_config(organization)
114
+ raise GithubOrganizationUnknown, organization if config.nil?
115
+ end
116
+ @github ||= {}
117
+ @github[organization] ||= GitHubApp.new(organization, config)
118
+ end
119
+
120
+ def github_default_organization
121
+ return nil unless secrets&.github
122
+ org = secrets.github.keys.first
123
+ TOP_LEVEL_GH_KEYS.include?(org) ? nil : org
124
+ end
125
+
126
+ def github_organizations
127
+ return [nil] unless github_default_organization
128
+ secrets.github.keys
129
+ end
130
+
131
+ def github_app_config(organization)
132
+ github_config = secrets.github.deep_transform_keys(&:downcase)
133
+ github_organization = organization.downcase.to_sym
134
+ github_config[github_organization]
109
135
  end
110
136
 
111
137
  def legacy_github_api
112
138
  if secrets&.github_api.present?
113
- @legacy_github_api ||= github.new_client(access_token: secrets.github_api['access_token'])
139
+ @legacy_github_api ||= github.new_client(access_token: secrets.github_api[:access_token])
114
140
  end
115
141
  end
116
142
 
@@ -175,13 +201,11 @@ module Shipit
175
201
  end
176
202
 
177
203
  def revision
178
- @revision ||= begin
179
- if revision_file.exist?
180
- revision_file.read
181
- else
182
- %x(git rev-parse HEAD)
183
- end.strip
184
- end
204
+ @revision ||= if revision_file.exist?
205
+ revision_file.read
206
+ else
207
+ %x(git rev-parse HEAD)
208
+ end.strip
185
209
  end
186
210
 
187
211
  def default_inactivity_timeout
@@ -13,7 +13,8 @@ module Shipit
13
13
  Denied = Class.new(Error)
14
14
  TimedOut = Class.new(Error)
15
15
 
16
- BASE_ENV = Bundler.clean_env.merge((ENV.keys - Bundler.clean_env.keys).map { |k| [k, nil] }.to_h)
16
+ unbundled_env = Bundler.respond_to?(:unbundled_env) ? Bundler.unbundled_env : Bundler.clean_env
17
+ BASE_ENV = unbundled_env.merge((ENV.keys - unbundled_env.keys).map { |k| [k, nil] }.to_h)
17
18
 
18
19
  class Failed < Error
19
20
  attr_reader :exit_code
@@ -29,7 +30,7 @@ module Shipit
29
30
  def initialize(*args, default_timeout: Shipit.default_inactivity_timeout, env: {}, chdir:)
30
31
  @args, options = parse_arguments(args)
31
32
  @timeout = options['timeout'] || options[:timeout] || default_timeout
32
- @env = env
33
+ @env = env.transform_values { |v| v&.to_s }
33
34
  @chdir = chdir.to_s
34
35
  @timed_out = false
35
36
  end
@@ -86,7 +87,7 @@ module Shipit
86
87
  @out = @pid = nil
87
88
  FileUtils.mkdir_p(@chdir)
88
89
  begin
89
- @out, child_in, @pid = PTY.spawn(clean_env, *interpolated_arguments, chdir: @chdir)
90
+ @out, child_in, @pid = PTY.spawn(unbundled_env, *interpolated_arguments, chdir: @chdir)
90
91
  child_in.close
91
92
  rescue Errno::ENOENT
92
93
  raise NotFound, "#{Shellwords.split(interpolated_arguments.first).first}: command not found"
@@ -97,8 +98,8 @@ module Shipit
97
98
  self
98
99
  end
99
100
 
100
- def clean_env
101
- BASE_ENV.merge('PATH' => "#{ENV['PATH']}:#{Shipit.shell_paths.join(':')}").merge(@env.stringify_keys)
101
+ def unbundled_env
102
+ BASE_ENV.merge('PATH' => "#{Shipit.shell_paths.join(':')}:#{ENV['PATH']}").merge(@env.stringify_keys)
102
103
  end
103
104
 
104
105
  def stream(&block)
@@ -216,7 +217,7 @@ module Shipit
216
217
  argument
217
218
  end
218
219
  end
219
- [args, options]
220
+ [args.map(&:to_s), options]
220
221
  end
221
222
 
222
223
  def running?
@@ -21,8 +21,8 @@ module Shipit
21
21
 
22
22
  def env
23
23
  @env ||= Shipit.env.merge(
24
- 'GITHUB_DOMAIN' => Shipit.github.domain,
25
- 'GITHUB_TOKEN' => Shipit.github.token,
24
+ 'GITHUB_DOMAIN' => github.domain,
25
+ 'GITHUB_TOKEN' => github.token,
26
26
  'GIT_ASKPASS' => Shipit::Engine.root.join('lib', 'snippets', 'git-askpass').realpath.to_s,
27
27
  )
28
28
  end
@@ -30,5 +30,12 @@ module Shipit
30
30
  def git(*args)
31
31
  Command.new("git", *args)
32
32
  end
33
+ ruby2_keywords :git if respond_to?(:ruby2_keywords, true)
34
+
35
+ private
36
+
37
+ def github
38
+ Shipit.github
39
+ end
33
40
  end
34
41
  end
data/lib/shipit/engine.rb CHANGED
@@ -10,6 +10,8 @@ module Shipit
10
10
  Shipit::Engine.routes.default_url_options[:host] = Shipit.host
11
11
  Pubsubstub.redis_url = Shipit.redis_url.to_s
12
12
 
13
+ Rails.application.secrets.deep_symbolize_keys!
14
+
13
15
  app.config.assets.paths << Emoji.images_path
14
16
  app.config.assets.precompile += %w(
15
17
  favicon.ico
data/lib/shipit/flock.rb CHANGED
@@ -11,14 +11,21 @@ module Shipit
11
11
 
12
12
  def initialize(path)
13
13
  @path = Pathname.new(path)
14
+ @acquired = false
14
15
  end
15
16
 
16
17
  def lock(timeout:)
18
+ return yield if @acquired
17
19
  path.parent.mkpath
18
20
  path.open('w') do |file|
19
21
  if retrying(timeout: timeout) { file.flock(File::LOCK_EX | File::LOCK_NB) }
20
22
  file.write($PROCESS_ID.to_s)
21
- return yield
23
+ @acquired = true
24
+ begin
25
+ yield
26
+ ensure
27
+ @acquired = false
28
+ end
22
29
  else
23
30
  raise TimeoutError, "Couldn't acquire lock for #{path} in #{timeout} seconds"
24
31
  end