Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking task into steps #37

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ gem "sqlite3"
gem "simplecov"
# Release management
gem "reissue"

group :development, :test do
gem "minitest-reporters"
end
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ansi (1.5.0)
ast (2.4.2)
base64 (0.2.0)
benchmark (0.4.0)
Expand Down Expand Up @@ -135,6 +136,11 @@ GEM
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.4)
minitest-reporters (1.7.1)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
multipart-post (2.4.1)
net-http (0.6.0)
uri
Expand Down Expand Up @@ -273,6 +279,7 @@ PLATFORMS
DEPENDENCIES
debug
discharger!
minitest-reporters
puma
reissue
simplecov
Expand Down
57 changes: 57 additions & 0 deletions lib/discharger/helpers/sys_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require "rainbow/refinement"

using Rainbow

module SysHelper
# Run a multiple system commands and return true if all commands succeed
# If any command fails, the method will return false and stop executing
# any further commands.
#
# Provide a block to evaluate the output of the command and return true
# if the command was successful. If the block returns false, the method
# will return false and stop executing any further commands.
#
# @param *steps [Array<Array<String>>] an array of commands to run
# @param block [Proc] a block to evaluate the output of the command
# @return [Boolean] true if all commands succeed, false otherwise
#
# @example
# syscall(
# ["echo Hello, World!"],
# ["ls -l"]
# )
def syscall(*steps, output: $stdout, error: $stderr)
success = false
stdout, stderr, status = nil
steps.each do |cmd|
puts cmd.join(" ").bg(:green).black
stdout, stderr, status = Open3.capture3(*cmd)
if status.success?
output.puts stdout
success = true
else
error.puts stderr
success = false
exit(status.exitstatus)
end
end
if block_given?
success = !!yield(stdout, stderr, status)
# If the error reports that a rule was bypassed, consider the command successful
# because we are bypassing the rule intentionally when merging the release branch
# to the production branch.
success = true if stderr.match?(/bypassed rule violations/i)
abort(stderr) unless success
end
success
end

# Echo a message to the console
#
# @param message [String] the message to echo
# return [TrueClass]
def sysecho(message, output: $stdout)
output.puts message
true
end
end
63 changes: 63 additions & 0 deletions lib/discharger/steps/prepare.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require "rainbow/refinement"

using Rainbow

module Discharger
module Steps
class Prepare
def initialize(task)
@task = task
@tasker = task.tasker
end

def prepare_for_release
@task.desc <<~DESC
---------- STEP 1 ----------
Prepare the current version for release

This task will check that the current version is ready for release by
verifying that the version number is valid and that the changelog is
up to date.
DESC
@task.task prepare: [:environment] do
current_version = Object.const_get(@task.version_constant)
@task.sysecho "Preparing version #{current_version} for release"

if @task.mono_repo && @task.gem_tag
@task.sysecho "Checking for gem tag #{@task.gem_tag}"
@task.syscall ["git fetch origin #{@task.gem_tag}"]
@task.syscall ["git tag -l #{@task.gem_tag}"]
end

@task.syscall(
["git fetch origin #{@task.working_branch}"],
["git checkout #{@task.working_branch}"],
["git pull origin #{@task.working_branch}"]
)

@task.sysecho "Checking changelog for version #{current_version}"
changelog = File.read(@task.changelog_file)
unless changelog.include?(current_version.to_s)
raise "Version #{current_version} not found in #{@task.changelog_file}"
end

@task.sysecho "Version #{current_version} is ready for release"
end
end

private

def method_missing(method_name, *args, &block)
if @task.respond_to?(method_name)
@task.send(method_name, *args, &block)
else
super
end
end

def respond_to_missing?(method_name, include_private = false)
@task.respond_to?(method_name, include_private) || super
end
end
end
end
162 changes: 162 additions & 0 deletions lib/discharger/steps/release.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
require "rainbow/refinement"

using Rainbow

module Discharger
module Steps
class Release
include Rake::DSL

def initialize(task)
@task = task
@tasker = task.tasker
end

def release_to_production
push_to_production
establish_config
build_environment
send_message_to_slack
end

private

def push_to_production
@task.desc <<~DESC
---------- STEP 3 ----------
Release the current version to production

This task rebases the production branch on the staging branch and tags the
current version. The production branch and the tag will be pushed to the
remote repository.

After the release is complete, a new branch will be created to bump the
version for the next release.
DESC
@task.task @task.name => [:environment] do
current_version = Object.const_get(@task.version_constant)
@task.sysecho <<~MSG
Releasing version #{current_version} to production.

This will tag the current version and push it to the production branch.
MSG
@task.sysecho "Are you ready to continue? (Press Enter to continue, Type 'x' and Enter to exit)".bg(:yellow).black
input = $stdin.gets
exit if input.chomp.match?(/^x/i)

continue = @task.syscall(
["git checkout #{@task.working_branch}"],
["git branch -D #{@task.staging_branch} 2>/dev/null || true"],
["git branch -D #{@task.production_branch} 2>/dev/null || true"],
["git fetch origin #{@task.staging_branch}:#{@task.staging_branch} #{@task.production_branch}:#{@task.production_branch}"],
["git checkout #{@task.production_branch}"],
["git reset --hard #{@task.staging_branch}"],
["git tag -a v#{current_version} -m 'Release #{current_version}'"],
["git push origin #{@task.production_branch}:#{@task.production_branch} v#{current_version}:v#{current_version}"],
["git push origin v#{current_version}"]
) do
@tasker["#{@task.name}:slack"].invoke(
"Released #{@task.app_name} #{current_version} to production.",
@task.release_message_channel,
":chipmunk:"
)
if @task.last_message_ts.present?
text = File.read(Rails.root.join(@task.changelog_file))
@tasker["#{@task.name}:slack"].reenable
@tasker["#{@task.name}:slack"].invoke(
text,
@task.release_message_channel,
":log:",
@task.last_message_ts
)
end
@task.syscall ["git checkout #{@task.working_branch}"]
end

abort "Release failed." unless continue

@task.sysecho <<~MSG
Version #{current_version} released to production.

Preparing to bump the version for the next release.

MSG

@tasker["reissue"].invoke
new_version = Object.const_get(@task.version_constant)
new_version_branch = "bump/begin-#{new_version.tr(".", "-")}"
continue = @task.syscall(["git checkout -b #{new_version_branch}"])

abort "Bump failed." unless continue

pr_url = "#{@task.pull_request_url}/compare/#{@task.working_branch}...#{new_version_branch}?expand=1&title=Begin%20#{current_version}"

@task.syscall(["git push origin #{new_version_branch} --force"]) do
@task.sysecho <<~MSG
Branch #{new_version_branch} created.

Open a PR to #{@task.working_branch} to mark the version and update the chaneglog
for the next release.

Opening PR: #{pr_url}
MSG
end.then do |success|
@task.syscall ["open #{pr_url}"] if success
end
end
end

def establish_config
@task.desc "Echo the configuration settings."
@task.task "#{@task.name}:config" do
@task.sysecho "-- Discharger Configuration --".bg(:green).black
@task.sysecho "SHA: #{@task.commit_identifier.call}".bg(:red).black
@task.instance_variables.sort.each do |var|
value = @task.instance_variable_get(var)
value = value.call if value.is_a?(Proc) && value.arity.zero?
@task.sysecho "#{var.to_s.sub("@", "").ljust(24)}: #{value}".bg(:yellow).black
end
@task.sysecho "----------------------------------".bg(:green).black
end
end

def build_environment
@task.desc @task.description
@task.task "#{@task.name}:build" => :environment do
@task.syscall(
["git fetch origin #{@task.working_branch}"],
["git checkout #{@task.working_branch}"],
["git branch -D #{@task.staging_branch} 2>/dev/null || true"],
["git checkout -b #{@task.staging_branch}"],
["git push origin #{@task.staging_branch} --force"]
) do
@tasker["#{@task.name}:slack"].invoke(
"Building #{@task.app_name} #{@task.commit_identifier.call} on #{@task.staging_branch}.",
@task.release_message_channel
)
@task.syscall ["git checkout #{@task.working_branch}"]
end
end
end

def send_message_to_slack
@task.desc "Send a message to Slack."
@task.task "#{@task.name}:slack", [:text, :channel, :emoji, :ts] => :environment do |_, args|
args.with_defaults(
channel: @task.release_message_channel,
emoji: nil
)
client = Slack::Web::Client.new
options = args.to_h
options[:icon_emoji] = options.delete(:emoji) if options[:emoji]
options[:thread_ts] = options.delete(:ts) if options[:ts]

@task.sysecho "Sending message to Slack:".bg(:green).black + " #{args[:text]}"
result = client.chat_postMessage(**options)
@task.instance_variable_set(:@last_message_ts, result["ts"])
@task.sysecho %(Message sent: #{result["ts"]})
end
end
end
end
end
55 changes: 55 additions & 0 deletions lib/discharger/steps/stage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
require "rainbow/refinement"
require "uri" # Add this for URL encoding

using Rainbow

module Discharger
module Steps
class Stage
def initialize(task)
@task = task
@tasker = task.tasker
end

def stage_release_branch
@task.desc <<~DESC
---------- STEP 2 ----------
Stage the current version for release

This task creates a staging branch from the current working branch and
pushes it to the remote repository. A pull request will be opened to merge
the staging branch into production.
DESC
@task.task stage: [:environment, :build] do
current_version = Object.const_get(@task.version_constant)
@task.sysecho "Branch staging updated"
@task.sysecho "Open a PR to production to release the version"
@task.sysecho "Once the PR is **approved**, run 'rake release' to release the version"

pr_url = "#{@task.pull_request_url}/compare/#{@task.production_branch}...#{@task.staging_branch}"
pr_params = {
expand: 1,
title: URI.encode_www_form_component("Release #{current_version} to production"),
body: URI.encode_www_form_component("Deploy #{current_version} to production.\n")
}.map { |k, v| "#{k}=#{v}" }.join("&")

@task.syscall ["open #{pr_url}?#{pr_params}"]
end
end

private

def method_missing(method_name, *args, &block)
if @task.respond_to?(method_name)
@task.send(method_name, *args, &block)
else
super
end
end

def respond_to_missing?(method_name, include_private = false)
@task.respond_to?(method_name, include_private) || super
end
end
end
end
Loading