|
| 1 | +#!/usr/bin/env ruby |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +# This script is used to revert a release that was created with the create-github-release gem |
| 5 | +# It will delete the release branch and tag locally and remotely |
| 6 | + |
| 7 | +require 'create_github_release' |
| 8 | + |
| 9 | +require 'English' |
| 10 | +require 'optparse' |
| 11 | + |
| 12 | +# Options for running this script |
| 13 | +class Options |
| 14 | + attr_writer :default_branch, :release_version, :release_tag, :release_branch, :current_branch, :remote |
| 15 | + |
| 16 | + def default_branch = @default_branch ||= 'main' |
| 17 | + def release_version = @release_version ||= `semverify current`.chomp |
| 18 | + def release_tag = @release_tag ||= "v#{release_version}" |
| 19 | + def release_branch = @release_branch ||= "release-#{release_tag}" |
| 20 | + def current_branch = @current_branch ||= `git rev-parse --abbrev-ref HEAD`.chomp |
| 21 | + def remote = @remote ||= 'origin' |
| 22 | +end |
| 23 | + |
| 24 | +# Parse the command line options for this script |
| 25 | +class Parser |
| 26 | + # Create a new command line parser |
| 27 | + # |
| 28 | + # @example |
| 29 | + # parser = CommandLineParser.new |
| 30 | + # |
| 31 | + def initialize |
| 32 | + @option_parser = OptionParser.new |
| 33 | + define_options |
| 34 | + @options = Options.new |
| 35 | + end |
| 36 | + |
| 37 | + attr_reader :option_parser, :options |
| 38 | + |
| 39 | + # Parse the command line arguements returning the options |
| 40 | + # |
| 41 | + # @example |
| 42 | + # options = Parser.new.parse(*ARGV) |
| 43 | + # |
| 44 | + # @param args [Array<String>] the command line arguments |
| 45 | + # |
| 46 | + # @return [Options] the options |
| 47 | + # |
| 48 | + def parse(*args) |
| 49 | + begin |
| 50 | + option_parser.parse!(remaining_args = args.dup) |
| 51 | + rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e |
| 52 | + report_errors(e.message) |
| 53 | + end |
| 54 | + parse_remaining_args(remaining_args) |
| 55 | + options |
| 56 | + end |
| 57 | + |
| 58 | + private |
| 59 | + |
| 60 | + # Output an error message and useage to stderr and exit |
| 61 | + # @return [void] |
| 62 | + # @api private |
| 63 | + def report_errors(*errors) |
| 64 | + warn error_message(errors) |
| 65 | + exit 1 |
| 66 | + end |
| 67 | + |
| 68 | + # The command line template as a string |
| 69 | + # @return [String] |
| 70 | + # @api private |
| 71 | + def command_template |
| 72 | + <<~COMMAND |
| 73 | + #{File.basename($PROGRAM_NAME)} [--help] |
| 74 | + COMMAND |
| 75 | + end |
| 76 | + |
| 77 | + DESCRIPTION = <<~DESCRIPTION |
| 78 | + This script reverts the effect of running the create-github-release script. |
| 79 | + It must be run in the root directory of the work tree with the release |
| 80 | + branch checked out (which is the state create-github-release leaves you in). |
| 81 | +
|
| 82 | + This script should be run beefore the release PR is merged. |
| 83 | +
|
| 84 | + This script removes the release branch and release tag both in the local |
| 85 | + repository and on the remote. Deleting the branch on GitHub will close the |
| 86 | + GitHub PR automatically. |
| 87 | + DESCRIPTION |
| 88 | + |
| 89 | + # Define the options for OptionParser |
| 90 | + # @return [void] |
| 91 | + # @api private |
| 92 | + def define_options |
| 93 | + option_parser.banner = "Usage: #{command_template}" |
| 94 | + option_parser.separator '' |
| 95 | + option_parser.separator DESCRIPTION |
| 96 | + option_parser.separator '' |
| 97 | + option_parser.separator 'Options:' |
| 98 | + |
| 99 | + %i[ |
| 100 | + define_help_option |
| 101 | + ].each { |m| send(m) } |
| 102 | + end |
| 103 | + |
| 104 | + # Define the help option |
| 105 | + # @return [void] |
| 106 | + # @api private |
| 107 | + def define_help_option |
| 108 | + option_parser.on_tail('-h', '--help', 'Show this message') do |
| 109 | + puts option_parser |
| 110 | + exit 0 |
| 111 | + end |
| 112 | + end |
| 113 | + |
| 114 | + # Parse non-option arguments |
| 115 | + # @return [void] |
| 116 | + # @api private |
| 117 | + def parse_remaining_args(remaining_args) |
| 118 | + # There should be no remaining args |
| 119 | + report_errors('Too many args') unless remaining_args.empty? |
| 120 | + end |
| 121 | +end |
| 122 | + |
| 123 | +def in_work_tree? = `git rev-parse --is-inside-work-tree 2>/dev/null`.chomp == 'true' |
| 124 | +def in_root_directory? = `git rev-parse --show-toplevel 2>/dev/null`.chomp == Dir.pwd |
| 125 | + |
| 126 | +def ref_exists?(name) |
| 127 | + `git rev-parse --verify #{name} >/dev/null 2>&1` |
| 128 | + $CHILD_STATUS.success? |
| 129 | +end |
| 130 | + |
| 131 | +unless in_work_tree? && in_root_directory? |
| 132 | + warn 'ERROR: Not in the root directory of a Git work tree' |
| 133 | + exit 1 |
| 134 | +end |
| 135 | + |
| 136 | +# Parse the command line options |
| 137 | +options = Parser.new.parse(*ARGV) |
| 138 | + |
| 139 | +unless options.release_branch == options.current_branch |
| 140 | + warn "ERROR: The current branch '#{options.current_branch}' is not the release branch for #{options.release_version}" |
| 141 | + exit 1 |
| 142 | +end |
| 143 | + |
| 144 | +unless ref_exists?(options.default_branch) |
| 145 | + warn "ERROR: The default branch '#{options.default_branch}' does not exist" |
| 146 | + exit 1 |
| 147 | +end |
| 148 | + |
| 149 | +`git checkout #{options.default_branch} >/dev/null` |
| 150 | +`git branch -D #{options.release_branch} >/dev/null` |
| 151 | +`git tag -d #{options.release_tag} >/dev/null` |
| 152 | +`git push #{options.remote} --delete #{options.release_branch} >/dev/null` |
| 153 | +`git push #{options.remote} --delete #{options.release_tag} >/dev/null` |
| 154 | + |
| 155 | +puts "Reverted release #{options.release_version}" |
0 commit comments