main-branch/create_github_release

View on GitHub
exe/revert-github-release

Summary

Maintainability
Test Coverage
#!/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}'"