Skip to content

Commit

Permalink
Retry publish command on failure
Browse files Browse the repository at this point in the history
When publishing a package and the command would fail, mono would exit
entirely. This would happen if a user would enter an invalid OTP code
during the process for example.

When in a mono package repo this could exit the entire process, such as
like on package 3 of 10. The only way to publish the package properly
then (as part was already published) is to publish them manually.

This retry option will prompt users on failure of the command to retry
it.

I've only added it to the publish command for now because that's the
only problematic one I can think of right now. If the build step fails
nothing is in a state of being half published yet. Pushing the Git
branch and tags should also be easy to do manually.

Fixes #24
  • Loading branch information
tombruijn committed Sep 28, 2021
1 parent 46ae0c0 commit 43cc5fc
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changesets/retry-failed-publish-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: "patch"
---

Retry failed publish commands. When a publish command like `gem push` fails, prompt the user to retry it rather than fail the entire publish process.
30 changes: 23 additions & 7 deletions lib/mono/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,39 @@ def execute
parts << command
cmd = parts.join

puts cmd
unless dry_run?
execute_command cmd unless dry_run?
end

private

def execute_command(cmd)
loop do
puts cmd
system cmd
exitstatus = $?
unless exitstatus.success?
puts "Error: Command failed with status `#{exitstatus.exitstatus}`"
exit 1
break if exitstatus.success?

if retry?
answer = Shell.yes_or_no(
"Error: Command failed. Do you want to retry? (Y/n): ",
:default => "Y"
)
next if answer
end

puts "Error: Command failed with status `#{exitstatus.exitstatus}`"
exit 1
end
end

private

def dry_run?
ENV["DRY_RUN"] == "true"
end

def retry?
options[:retry]
end

def path
dir = options[:dir]
dir if dir && dir != "."
Expand Down
2 changes: 1 addition & 1 deletion lib/mono/languages/elixir/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def bootstrap_package(_options = {})
end

def publish_package
run_command_in_package "mix hex.publish --yes"
run_command_in_package "mix hex.publish --yes", :retry => true
end

def build_package
Expand Down
7 changes: 4 additions & 3 deletions lib/mono/languages/nodejs/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def publish_package
options << "--tag #{next_version.prerelease_type}"
end
options << "--new-version #{next_version}" if npm_client == "yarn"
run_client_command_in_package "publish #{options.join(" ")}".strip
run_client_command_in_package "publish #{options.join(" ")}".strip,
:retry => true
end

def build_package
Expand Down Expand Up @@ -108,8 +109,8 @@ def run_client_command(command)
run_command "#{npm_client} #{command}"
end

def run_client_command_in_package(command)
run_command_in_package "#{npm_client} #{command}"
def run_client_command_in_package(command, options = {})
run_command_in_package "#{npm_client} #{command}", options
end

def run_client_command_for_package(command)
Expand Down
2 changes: 1 addition & 1 deletion lib/mono/languages/ruby/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def publish_package
gem_files = fetch_gem_files_paths
if gem_files.any?
gem_files.each do |gem_file|
run_command "gem push #{gem_file}"
run_command "gem push #{gem_file}", :retry => true
end
else
raise "No gemfiles found in `#{gem_files_dir || "."}`"
Expand Down
6 changes: 3 additions & 3 deletions lib/mono/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def build
def publish_next_version
if config.command?("publish")
# Custom command configured
run_command_in_package config.command("publish")
run_command_in_package config.command("publish"), :retry => true
else
publish_package
end
Expand Down Expand Up @@ -180,8 +180,8 @@ def unbootstrap_package
end
# :nocov:

def run_command_in_package(command)
run_command command, :dir => path
def run_command_in_package(command, options = {})
run_command command, { :dir => path }.merge(options)
end

def build_tag(version)
Expand Down
47 changes: 43 additions & 4 deletions spec/lib/mono/cli/publish/elixir_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,42 @@
])
expect(exit_status).to eql(0), output
end

context "with failing publish command" do
it "retries to publish" do
fail_command = "exit 1"
prepare_elixir_project do
create_package_mix :version => "1.2.3"
add_changeset :patch
end
confirm_publish_package
add_cli_input "y" # Retry command
add_cli_input "n" # Don't retry command
output = run_publish_process(
:stubbed_commands => [/^git push/],
:failed_commands => [/^mix hex.publish package --yes/]
)

project_dir = "/#{current_project}"
next_version = "1.2.4"

expect(output).to include(<<~OUTPUT), output
#{fail_command}
Error: Command failed. Do you want to retry? (Y/n): #{fail_command}
Error: Command failed. Do you want to retry? (Y/n): Error: Command failed with status `1`
OUTPUT

expect(performed_commands).to eql([
[project_dir, "mix deps.get"],
[project_dir, "mix compile"],
[project_dir, "git add -A"],
[project_dir, "git commit -m 'Publish packages' -m '- v#{next_version}' -m '[ci skip]'"],
[project_dir, "git tag v#{next_version}"],
[project_dir, "mix hex.publish package --yes"]
])
expect(exit_status).to eql(1), output
end
end
end

context "with mono Elixir project" do
Expand Down Expand Up @@ -278,15 +314,18 @@ def prepare_elixir_project(config = {})
end
end

def run_publish_process
def run_publish_process(failed_commands: [], stubbed_commands: nil)
stubbed_commands ||= [/^mix hex.publish package --yes/, /^git push/]
capture_stdout do
in_project do
add_changeset(:patch)

perform_commands do
stub_commands [/^mix hex.publish package --yes/, /^git push/] do
run_bootstrap
run_publish
fail_commands failed_commands do
stub_commands stubbed_commands do
run_bootstrap
run_publish
end
end
end
end
Expand Down
49 changes: 45 additions & 4 deletions spec/lib/mono/cli/publish/nodejs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,44 @@
])
expect(exit_status).to eql(0), output
end

context "with failing publish command" do
it "retries to publish" do
fail_command = "exit 1"
prepare_nodejs_project do
create_package_json :version => "1.2.3"
add_changeset :patch
end
confirm_publish_package
add_cli_input "y" # Retry command
add_cli_input "n" # Don't retry command
output = run_publish_process(
:stubbed_commands => [/^git push/],
:failed_commands => [/^npm publish/]
)

project_dir = "/#{current_project}"
next_version = "1.2.4"

expect(output).to include(<<~OUTPUT), output
#{fail_command}
Error: Command failed. Do you want to retry? (Y/n): #{fail_command}
Error: Command failed. Do you want to retry? (Y/n): Error: Command failed with status `1`
OUTPUT

expect(performed_commands).to eql([
[project_dir, "npm install"],
[project_dir, "npm link"],
[project_dir, "npm run build"],
[project_dir, "git add -A"],
[project_dir,
"git commit -m 'Publish packages' -m '- v#{next_version}' -m '[ci skip]'"],
[project_dir, "git tag v#{next_version}"],
[project_dir, "npm publish"]
])
expect(exit_status).to eql(1), output
end
end
end

context "with mono Node.js project" do
Expand Down Expand Up @@ -746,13 +784,16 @@ def prepare_nodejs_project(config = {})
end
end

def run_publish_process(args = [])
def run_publish_process(args = [], failed_commands: [], stubbed_commands: nil)
stubbed_commands ||= [/^(npm|yarn) publish/, /^git push/]
capture_stdout do
in_project do
perform_commands do
stub_commands [/^(npm|yarn) publish/, /^git push/] do
run_bootstrap
run_publish(args)
fail_commands failed_commands do
stub_commands stubbed_commands do
run_bootstrap
run_publish(args)
end
end
end
end
Expand Down
43 changes: 40 additions & 3 deletions spec/lib/mono/cli/publish/ruby_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,41 @@
])
expect(exit_status).to eql(0), output
end

context "with failing publish command" do
it "retries to publish" do
fail_command = "exit 1"
prepare_ruby_project do
create_ruby_package_files :name => "mygem", :version => "1.2.3"
add_changeset :patch
end
confirm_publish_package
add_cli_input "y" # Retry command
add_cli_input "n" # Don't retry command
output = run_publish_process(
:stubbed_commands => [/^git push/],
:failed_commands => [/^gem push/]
)

project_dir = "/#{current_project}"
next_version = "1.2.4"

expect(output).to include(<<~OUTPUT), output
#{fail_command}
Error: Command failed. Do you want to retry? (Y/n): #{fail_command}
Error: Command failed. Do you want to retry? (Y/n): Error: Command failed with status `1`
OUTPUT

expect(performed_commands).to eql([
[project_dir, "gem build"],
[project_dir, "git add -A"],
[project_dir, "git commit -m 'Publish packages' -m '- v#{next_version}' -m '[ci skip]'"],
[project_dir, "git tag v#{next_version}"],
[project_dir, "gem push mygem-#{next_version}.gem"]
])
expect(exit_status).to eql(1), output
end
end
end

context "with mono Ruby package" do
Expand Down Expand Up @@ -440,12 +475,14 @@ def prepare_ruby_project(config = {})
end
end

def run_publish_process
def run_publish_process(failed_commands: [], stubbed_commands: [/^gem push/, /^git push/])
capture_stdout do
in_project do
perform_commands do
stub_commands [/^gem push/, /^git push/] do
run_publish
fail_commands failed_commands do
stub_commands stubbed_commands do
run_publish
end
end
end
end
Expand Down
8 changes: 8 additions & 0 deletions spec/support/helpers/command_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def stub_commands(command_matchers)
yield
end

# When a command is wrapped with {perform_commands} they are actually
# executed. If you want cause one to fail deliberatly (to test the error
# handling), use this helper.
def fail_commands(command_matchers)
command_matchers.each { |matcher| Testing.failed_commands << matcher }
yield
end

def exit_status
Testing.exit_status || 0
end
Expand Down
8 changes: 8 additions & 0 deletions spec/testing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def perform_commands?
def stubbed_commands
@stubbed_commands ||= []
end

def failed_commands
@failed_commands ||= []
end
end

module Command
Expand All @@ -54,6 +58,10 @@ def execute
return unless Testing.perform_commands?
return if Testing.stubbed_commands.find { |matcher| matcher.match?(command) }

# Overwrite deliberatly failing commands with `exit 1` to force the
# failure
@command = "exit 1" if Testing.failed_commands.find { |matcher| matcher.match?(command) }

super
end
end
Expand Down

0 comments on commit 43cc5fc

Please sign in to comment.