diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..0bd1d80 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +group :development do + gem "rubocop" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..715786a --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,39 @@ +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + json (2.7.2) + language_server-protocol (3.17.0.3) + parallel (1.24.0) + parser (3.3.0.5) + ast (~> 2.4.1) + racc + racc (1.7.3) + rainbow (3.1.1) + regexp_parser (2.9.0) + rexml (3.2.6) + rubocop (1.63.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.2) + parser (>= 3.3.0.4) + ruby-progressbar (1.13.0) + unicode-display_width (2.5.0) + +PLATFORMS + arm64-darwin-22 + ruby + +DEPENDENCIES + rubocop + +BUNDLED WITH + 2.5.6 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1feca93 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# deploy-tools + +## Development + +Install dependencies. + +```shell +bundle install +``` + +Run rubocop (including on the `.gemspec` file). + +```shell +bundle exec rubocop +``` + +Build the gem. (Example for `v0.2.0`) + +```shell +$ gem build deploy-tools.gemspec +WARNING: licenses is empty, but is recommended. Use an license identifier from +https://spdx.org/licenses or 'Nonstandard' for a nonstandard license, +or set it to nil if you don't want to specify a license. +WARNING: See https://guides.rubygems.org/specification-reference/ for help + Successfully built RubyGem + Name: deploy-tools + Version: 0.2.0 + File: deploy-tools-0.2.0.gem +``` + +Create a [release on the Github repo](https://github.com/simplepractice/deploy-tools/releases). Attach the built gem file as a release asset. + +## Usage + +To see installation and usage of the `deploy-tools` gem, see examples in `Makefile`s in deployable repos. + +Example: [simplepractice/simplepractice Makefile L6L16-L20 (ff81396)](https://github.com/simplepractice/simplepractice/blob/ff8139631c546804aae9a9577ee04bc18b21c987/Makefile#L6-L20) + +```shell +DEPLOY_TOOLS_VERSION = 0.1.4 +# ... +echo "Downloading deploy-tools gem" +wget -O deploy-tools.gem https://github.com/simplepractice/deploy-tools/releases/download/$(DEPLOY_TOOLS_VERSION)/deploy-tools-$(DEPLOY_TOOLS_VERSION).gem +echo "Installing gem" +gem install deploy-tools.gem +rm deploy-tools.gem +``` diff --git a/bin/prompt_for_unusual_local_git_state b/bin/prompt_for_unusual_local_git_state new file mode 100755 index 0000000..c23ff9c --- /dev/null +++ b/bin/prompt_for_unusual_local_git_state @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +# Hacky wrapper file to call the corresponding shell script. By default, installed gems run scripts as if they are Ruby, +# and do not respect the shebang line (e.g. a bash shebang). This file is a workaround to call the shell script directly. +# Hack source: https://stackoverflow.com/a/27988355/2291928 + +system("#{__FILE__}.sh #{ARGV.join(' ')}") +exit $?.exitstatus diff --git a/bin/prompt_for_unusual_local_git_state.sh b/bin/prompt_for_unusual_local_git_state.sh new file mode 100755 index 0000000..80f048f --- /dev/null +++ b/bin/prompt_for_unusual_local_git_state.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Checks local git state for unusual conditions and prompts the user to confirm before deploying. +# +# This is a safety measure to prevent accidental deployments of unintended target commits. +# The purpose is to draw attention to unusual conditions to be reviewed carefully. +# +# This script is intended to be used as a pre-deploy hook in a deployment script for manual/local deployments. +# It is not intended for automated deployments. Set CI=true to skip. +# +# Releases typically deploy the HEAD of the release branch. The conditions in this script favor that typical case. +# There are valid situations (rollback, hotfix, temporary config changes) which trigger warnings. That's OK/expected. +# In such situations, the user should review carefully and confirm the deployment at their discretion. +# +# Usage: $0 +# - git_remote: The remote to fetch the release branch from e.g. "origin". +# - git_release_branch: The release branch to compare the target REVISION against e.g. "main". +# - revision: The specific commit hash being deployed e.g. "41af8fa0fc80b7f590a0f46eccd4956b4a45c932". + +if [ "${CI}" = "true" ]; then + echo "CI environment detected. Skipping check for unusual local git state." + exit 0 +fi + +git_remote="$1" +git_release_branch="$2" +revision="$3" + +if [ -z "${git_remote}" ] || [ -z "${git_release_branch}" ] || [ -z "${revision}" ]; then + echo "ERROR: Missing required arguments. Usage: $0 " + exit 1 +fi + +set -euo pipefail + +git fetch "${git_remote}" "${git_release_branch}" || { echo "ERROR: git fetch failed."; exit 1; } + +show_warning_prompt='false' + +echo "Checking if there are any uncommitted changes..." +if [ -z "$(git status --porcelain)" ]; then + echo "(check passed) Git state is clean. There are no uncommitted changes." +else + show_warning_prompt='true' + echo "WARNING: Uncommitted changes detected!" + echo "⬇ Review the git status. ⬇" + git status + echo +fi + +echo "Checking if the currently checked out branch is the release branch ${git_release_branch}..." +current_branch=$(git rev-parse --abbrev-ref HEAD) +if [ "${current_branch}" = "${git_release_branch}" ]; then + echo "(check passed) On ${current_branch} branch." +else + show_warning_prompt='true' + echo "WARNING: Not on ${git_release_branch} branch! Currently on ${current_branch} branch." + echo "⬇ Review the git log for the currently checked out commit. ⬇" + git --no-pager log -1 + echo +fi + +echo "Checking if REVISION=${revision} is the HEAD of the up-to-date remote release branch ${git_remote}/${git_release_branch}..." +remote_release_branch_head="$(git rev-parse "${git_remote}/${git_release_branch}")" +if [ "${revision}" = "${remote_release_branch_head}" ]; then + echo "(check passed) REVISION=${revision} is the HEAD of the up-to-date remote release branch ${git_remote}/${git_release_branch}." +else + show_warning_prompt='true' + + remote_release_branch_relative_time="$(git --no-pager log -1 --pretty=format:'%cr' "${remote_release_branch_head}")" + revision_relative_time="$(git --no-pager log -1 --pretty=format:'%cr' "${revision}")" + + echo "WARNING: REVISION is not the HEAD of the remote release branch ${git_remote}/${git_release_branch}!" + echo "- ${remote_release_branch_head} ${git_remote}/${git_release_branch} (${remote_release_branch_relative_time})" + echo "- ${revision} REVISION (${revision_relative_time})" + echo "⬇ Review the git log for ${git_remote}/${git_release_branch} (committed ${remote_release_branch_relative_time}). ⬇" + git --no-pager log -1 "${git_remote}/${git_release_branch}" + echo "⬇ Review the git log for REVISION (committed ${revision_relative_time}). ⬇" + git --no-pager log -1 "${revision}" + echo +fi + +if [ "${show_warning_prompt}" = 'true' ]; then + printf "WARNING: Unusual conditions detected! Are you sure you want to deploy? (y/n) " + read -r prompt_answer + if [ "${prompt_answer}" != "y" ]; then + echo "Aborting." + exit 1 + fi +fi diff --git a/deploy-tools.gemspec b/deploy-tools.gemspec index 1223a24..7963f8b 100644 --- a/deploy-tools.gemspec +++ b/deploy-tools.gemspec @@ -1,13 +1,18 @@ Gem::Specification.new do |s| - s.name = 'deploy-tools' - s.version = '0.1.4' - s.date = '2022-06-28' + s.name = "deploy-tools" + s.version = "0.2.0" + s.required_ruby_version = ">= 2.7" s.summary = "Deploy tools" s.description = "A set of script used for deployment" - s.authors = ["Tony Nyurkin", "Serhii Voronoi"] - s.email = 'tony@simplepractice.com' - s.files = `git ls-files `.split("\n") + s.authors = ["Tony Nyurkin", "Serhii Voronoi", "George Pantazes"] + s.email = "infra@simplepractice.com" + s.homepage = "https://github.com/simplepractice/deploy-tools" + s.files = ["README.md", *Dir.glob("bin/*")] s.bindir = "bin" - s.executables = ["blue_green_switch", "detect_inactive_color"] - s.add_runtime_dependency 'aws-sdk-elasticloadbalancingv2', '~>1.44' + s.executables = [ + "blue_green_switch", + "detect_inactive_color", + "prompt_for_unusual_local_git_state" + ] + s.add_runtime_dependency "aws-sdk-elasticloadbalancingv2", "~>1.44" end