exe/revert-github-release
#!/usr/bin/env ruby
# frozen_string_literal: true
# This script is used to revert a release that was created with the create-github-release gem
# It will delete the release branch and tag locally and remotely
require 'create_github_release'
require 'create_github_release/version'
require 'English'
require 'optparse'
# Options for running this script
class Options
attr_writer :default_branch, :release_version, :release_tag, :release_branch, :current_branch, :remote
def default_branch = @default_branch ||= 'main'
def release_version = @release_version ||= `gem-version-boss current`.chomp
def release_tag = @release_tag ||= "v#{release_version}"
def release_branch = @release_branch ||= "release-#{release_tag}"
def current_branch = @current_branch ||= `git rev-parse --abbrev-ref HEAD`.chomp
def release_pr = @release_pr ||= `gh pr list --search "head:#{release_branch}" --json number --jq ".[].number"`.chomp
def remote = @remote ||= 'origin'
end
# Parse the command line options for this script
class Parser
# Create a new command line parser
#
# @example
# parser = CommandLineParser.new
#
def initialize
@option_parser = OptionParser.new
define_options
@options = Options.new
end
attr_reader :option_parser, :options
# Parse the command line arguements returning the options
#
# @example
# options = Parser.new.parse(*ARGV)
#
# @param args [Array<String>] the command line arguments
#
# @return [Options] the options
#
def parse(*args)
begin
option_parser.parse!(remaining_args = args.dup)
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
report_errors(e.message)
end
parse_remaining_args(remaining_args)
options
end
private
# Output an error message and useage to stderr and exit
# @return [void]
# @api private
def report_errors(*errors)
warn error_message(errors)
exit 1
end
# The command line template as a string
# @return [String]
# @api private
def command_template
<<~COMMAND
#{File.basename($PROGRAM_NAME)} [--help]
COMMAND
end
BANNER = <<~BANNER.freeze
Usage:
#{File.basename($PROGRAM_NAME)} [--help | --version]
Version #{CreateGithubRelease::VERSION}
This script reverts the effect of running the create-github-release script.
It must be run in the root directory of the work tree with the release
branch checked out (which is the state create-github-release leaves you in).
This script should be run before the release PR is merged.
This script does the following:
* Deletes the local and remote release branch
* Deletes the local and remote release tag
* Deletes the GitHub release object
* Closes the GitHub release PR
Options:
BANNER
# Define the options for OptionParser
# @return [void]
# @api private
def define_options
option_parser.banner = BANNER
%i[
define_help_option define_version_option
].each { |m| send(m) }
end
# Define the help option
# @return [void]
# @api private
def define_help_option
option_parser.on_tail('-h', '--help', 'Show this message') do
puts option_parser
exit 0
end
end
# Define the version option
# @return [void]
# @api private
def define_version_option
option_parser.on_tail('-v', '--version', 'Output the version of this script') do
puts CreateGithubRelease::VERSION
exit 0
end
end
# Parse non-option arguments
# @return [void]
# @api private
def parse_remaining_args(remaining_args)
# There should be no remaining args
report_errors('Too many args') unless remaining_args.empty?
end
end
def in_work_tree? = `git rev-parse --is-inside-work-tree 2>/dev/null`.chomp == 'true'
def in_root_directory? = `git rev-parse --show-toplevel 2>/dev/null`.chomp == Dir.pwd
def ref_exists?(name)
`git rev-parse --verify #{name} >/dev/null 2>&1`
$CHILD_STATUS.success?
end
def revert_release!(options)
`gh pr comment #{options.release_pr} --body="Reverting this release using revert-github-release"`
`git checkout #{options.default_branch} >/dev/null`
`git branch -D #{options.release_branch} >/dev/null`
`git tag -d #{options.release_tag} >/dev/null`
`git push #{options.remote} --delete #{options.release_branch} >/dev/null`
`git push #{options.remote} --delete #{options.release_tag} >/dev/null`
`gh release delete #{options.release_tag} --yes >/dev/null`
end
unless in_work_tree? && in_root_directory?
warn 'ERROR: Not in the root directory of a Git work tree'
exit 1
end
# Parse the command line options
options = Parser.new.parse(*ARGV)
unless options.release_branch == options.current_branch
warn "ERROR: The current branch '#{options.current_branch}' is not the release branch for #{options.release_version}"
exit 1
end
unless ref_exists?(options.default_branch)
warn "ERROR: The default branch '#{options.default_branch}' does not exist"
exit 1
end
revert_release!(options)
puts "SUCCESS: reverted release '#{options.release_version}'"