From 16730398b7779997a7b11813913e994b4114656f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bryan=E2=84=A2?= <61433408+brycahta@users.noreply.github.com> Date: Mon, 8 Feb 2021 14:49:48 -0600 Subject: [PATCH] add automated release targets (#41) --- .github/workflows/ci.yaml | 17 ++ Makefile | 30 +++ scripts/create-local-tag-for-release | 150 ++++++++++++++ scripts/draft-release-notes | 28 +++ scripts/prepare-for-release | 295 +++++++++++++++++++++++++++ 5 files changed, 520 insertions(+) create mode 100755 scripts/create-local-tag-for-release create mode 100755 scripts/draft-release-notes create mode 100755 scripts/prepare-for-release diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c87c954..4e7d333 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,3 +60,20 @@ jobs: - name: Release Assets run: make release + + postRelease: + name: Post Release + runs-on: ubuntu-20.04 + needs: [release] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ${{ env.DEFAULT_GO_VERSION }} + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Sync to Homebrew + run: make homebrew-sync \ No newline at end of file diff --git a/Makefile b/Makefile index c76efaf..7c914ee 100644 --- a/Makefile +++ b/Makefile @@ -127,3 +127,33 @@ release: build-binaries build-docker-images upload-resources-to-github help: @grep -E '^[a-zA-Z_-]+:.*$$' $(MAKEFILE_LIST) | sort + +## Targets intended to be run in preparation for a new release +draft-release-notes: + ${MAKEFILE_PATH}/scripts/draft-release-notes + +create-local-release-tag-major: + ${MAKEFILE_PATH}/scripts/create-local-tag-for-release -m + +create-local-release-tag-minor: + ${MAKEFILE_PATH}/scripts/create-local-tag-for-release -i + +create-local-release-tag-patch: + ${MAKEFILE_PATH}/scripts/create-local-tag-for-release -p + +create-release-prep-pr: + ${MAKEFILE_PATH}/scripts/prepare-for-release + +create-release-prep-pr-draft: + ${MAKEFILE_PATH}/scripts/prepare-for-release -d + +release-prep-major: create-local-release-tag-major create-release-prep-pr + +release-prep-minor: create-local-release-tag-minor create-release-prep-pr + +release-prep-patch: create-local-release-tag-patch create-release-prep-pr + +release-prep-custom: # Run make NEW_VERSION=v1.2.3 release-prep-custom to prep for a custom release version +ifdef NEW_VERSION + $(shell echo "${MAKEFILE_PATH}/scripts/create-local-tag-for-release -v $(NEW_VERSION) && echo && make create-release-prep-pr") +endif \ No newline at end of file diff --git a/scripts/create-local-tag-for-release b/scripts/create-local-tag-for-release new file mode 100755 index 0000000..0adeb5e --- /dev/null +++ b/scripts/create-local-tag-for-release @@ -0,0 +1,150 @@ +#!/bin/bash + +# Script to create a new local tag in preparation for a release +# This script is idempotent i.e. it always fetches remote tags to create the new tag. +# E.g. If the current remote release tag is v1.0.0, +## 1) running `create-local-tag-for-release -p` will create a new tag v1.0.1 +## 2) immediately running `create-local-tag-for-release -m` will create a new tag v2.0.0 + +set -euo pipefail + +REPO_ROOT_PATH="$( cd "$(dirname "$0")"; cd ../; pwd -P )" +MAKEFILE_PATH=$REPO_ROOT_PATH/Makefile +TAG_REGEX="^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]*)?$" + +HELP=$(cat << 'EOM' + Create a new local tag in preparation for a release. This script is idempotent i.e. it always fetches remote tags to create the new tag. + + Usage: create-local-tag-for-release [options] + + Options: + -v new tag / version number. The script relies on the user to specify a valid and accurately incremented tag. + -m increment major version + -i increment minor version + -p increment patch version + -h help + + Examples: + create-local-tag-for-release -v v1.0.0 Create local tag for new version v1.0.0 + create-local-tag-for-release -i Create local tag for new version by incrementing minor version only (previous tag=v1.0.0, new tag=v1.1.0) +EOM +) + +MAJOR_INC=false +MINOR_INC=false +PATCH_INC=false +NEW_TAG="" +CURR_REMOTE_RELEASE_TAG="" + +process_args() { + while getopts "hmipv:" opt; do + case ${opt} in + h ) + echo -e "$HELP" 1>&2 + exit 0 + ;; + m ) + MAJOR_INC=true + ;; + i ) + MINOR_INC=true + ;; + p ) + PATCH_INC=true + ;; + v ) + NEW_TAG="${OPTARG}" + ;; + \? ) + echo "$HELP" 1>&2 + exit 0 + ;; + esac + done +} + +validate_args() { + if [[ ! -z $NEW_TAG ]]; then + if ! [[ $NEW_TAG =~ $TAG_REGEX ]]; then + echo "❌ Invalid new tag specified $NEW_TAG. Examples: v1.2.3, v1.2.3-dirty" + exit 1 + fi + + echo "πŸ₯‘ Using the new tag specified with -v flag. All other flags, if specified, will be ignored." + echo " NOTE:The script relies on the user to specify a valid and accurately incremented tag." + return + fi + + if ($MAJOR_INC && $MINOR_INC) || ($MAJOR_INC && $PATCH_INC) || ($MINOR_INC && $PATCH_INC); then + echo "❌ Invalid arguments passed. Specify only one of 3 tag parts to increment for the new tag: -m (major) or -i (minor) or -p (patch)." + exit 1 + fi + + if $MAJOR_INC || $MINOR_INC || $PATCH_INC; then + return + fi + + echo -e "❌ Invalid arguments passed. Specify atleast one argument.\n$HELP" + exit 1 +} + +sync_local_tags_from_remote() { + # setup remote upstream tracking to fetch tags + git remote add the-real-upstream https://github.com/awslabs/aws-simple-ec2-cli.git &> /dev/null || true + git fetch the-real-upstream + + # delete all local tags + git tag -l | xargs git tag -d + + # fetch remote tags + git fetch the-real-upstream --tags + + # record the latest release tag in remote, before creating a new tag + CURR_REMOTE_RELEASE_TAG=$(get_latest_tag) + + # clean up tracking + git remote remove the-real-upstream +} + +create_tag() { + git tag $NEW_TAG + echo -e "\nβœ… Created new tag $NEW_TAG (Current latest release tag in remote: v$CURR_REMOTE_RELEASE_TAG)\n" + exit 0 +} + +get_latest_tag() { + make -s -f $MAKEFILE_PATH latest-release-tag | cut -b 2- +} + +main() { + process_args "$@" + validate_args + + sync_local_tags_from_remote + + # if new tag is specified, create it + if [[ ! -z $NEW_TAG ]]; then + create_tag + fi + + # increment version + if $MAJOR_INC || $MINOR_INC || $PATCH_INC; then + curr_major_v=$(echo $CURR_REMOTE_RELEASE_TAG | tr '.' '\n' | head -1) + curr_minor_v=$(echo $CURR_REMOTE_RELEASE_TAG | tr '.' '\n' | head -2 | tail -1) + curr_patch_v=$(echo $CURR_REMOTE_RELEASE_TAG | tr '.' '\n' | tail -1) + + if [[ $MAJOR_INC == true ]]; then + new_major_v=$(echo $(($curr_major_v + 1))) + NEW_TAG=$(echo v$new_major_v.0.0) + elif [[ $MINOR_INC == true ]]; then + new_minor_v=$(echo $(($curr_minor_v + 1))) + NEW_TAG=$(echo v$curr_major_v.$new_minor_v.0) + elif [[ $PATCH_INC == true ]]; then + new_patch_v=$(echo $(($curr_patch_v + 1))) + NEW_TAG=$(echo v$curr_major_v.$curr_minor_v.$new_patch_v) + fi + create_tag + fi +} + +main "$@" diff --git a/scripts/draft-release-notes b/scripts/draft-release-notes new file mode 100755 index 0000000..dc6c5a5 --- /dev/null +++ b/scripts/draft-release-notes @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +GIT_REPO_ROOT=$(git rev-parse --show-toplevel) +BUILD_DIR="${GIT_REPO_ROOT}/build" + +RELEASE_NOTES="${BUILD_DIR}/release-notes.md" +touch "${RELEASE_NOTES}" + +>&2 git fetch --all --tags + +if git describe HEAD --tags | grep -Eq "^v[0-9]+(\.[0-9]+)*(-[a-z0-9]+)?$"; then + LAST_RELEASE_HASH=$(git rev-list --tags --max-count=1 --skip=1 --no-walk) +else + TAG=$(git describe HEAD --tags | grep -Eo "^v[0-9]+(\.[0-9]+)*") + LAST_RELEASE_HASH=$(git rev-list -1 $TAG) +fi + +echo "## Changes" | tee -a "${RELEASE_NOTES}" +for change in $(git rev-list $LAST_RELEASE_HASH..HEAD); do + one_line_msg=$(git --no-pager log --pretty='%s (thanks to %an)' "${change}" -n1 | sed 's/^\[.*\]//' | xargs) + # render markdown links for cross-posting release notes + pr_num=$(echo $one_line_msg | grep -Eo '(#[0-9]*)' || [[ $? == 1 ]]) + md_link="[$pr_num](https://github.com/awslabs/aws-simple-ec2-cli/pull/${pr_num:1})" + echo " - ${one_line_msg/\($pr_num\)/$md_link}" | tee -a "${RELEASE_NOTES}" +done + +>&2 echo -e "\n\nRelease notes file: ${RELEASE_NOTES}" diff --git a/scripts/prepare-for-release b/scripts/prepare-for-release new file mode 100755 index 0000000..a647186 --- /dev/null +++ b/scripts/prepare-for-release @@ -0,0 +1,295 @@ +#!/bin/bash + +# Script to: +## 1) create and checkout a new branch with the latest tag name +## 2) update Simple-EC2 version +## 3) commit release prep changes to new branch +## 4) create a PR from the new branch to upstream/main + +set -euo pipefail + +REPO_ROOT_PATH="$( cd "$(dirname "$0")"; cd ../; pwd -P )" +MAKEFILE_PATH=$REPO_ROOT_PATH/Makefile +LATEST_VERSION=$(make -s -f "$MAKEFILE_PATH" latest-release-tag | cut -b 2- ) +PREVIOUS_VERSION=$(make -s -f "$MAKEFILE_PATH" previous-release-tag | cut -b 2- ) +os=$(uname) + +# files with versions, to update +REPO_README=$REPO_ROOT_PATH/README.md +FILES=("$REPO_README") +FILES_CHANGED=() + +# release prep +LATEST_TAG="v$LATEST_VERSION" +NEW_BRANCH="pr/$LATEST_TAG-release" +COMMIT_MESSAGE="πŸ₯‘πŸ€– $LATEST_TAG release prep πŸ€–πŸ₯‘" +RELEASE_PREP=true + +# PR details +DEFAULT_REPO_FULL_NAME=$(make -s -f "$MAKEFILE_PATH" repo-full-name) +PR_BASE=main # target +PR_TITLE="πŸ₯‘πŸ€– $LATEST_TAG release prep" +PR_BODY="πŸ₯‘πŸ€– Auto-generated PR for $LATEST_TAG release. Updating release versions in repo." +PR_LABEL_1="release-prep" +PR_LABEL_2="πŸ€– auto-generatedπŸ€–" +EC2_BOT_USER="ec2-bot πŸ€–" +EC2_BOT_EMAIL="ec2-bot@users.noreply.github.com" +EC2_BOT_SET=false + +HELP=$(cat << 'EOM' + Update repo with the new release version and create a pr from a new release prep branch. + This script prompts the user with complete details about the PR before pushing the new local branch to remote and creating the PR. + The new release version is the latest local git tag. + Note: The local tag creation for a new release is separated from this script. A new tag must be created before this script is run which is automated when this script is run via make targets. + + Usage: prepare-for-release [options] + + Options: + -d create a draft pr + -m update versions in ReadMe only + -r target repo full name for the pr (default: awslabs/aws-simple-ec2-cli) + -h help + + Examples: + prepare-for-release -d update release version in repo and create a draft pr against awslabs/aws-simple-ec2-cli + prepare-for-release -r username/aws-simple-ec2-cli update release version in repo and create a pr against username/aws-simple-ec2-cli +EOM +) + +DRAFT=false +REPO_FULL_NAME="" +NEED_ROLLBACK=true + +process_args() { + while getopts "hdmr:" opt; do + case ${opt} in + h ) + echo -e "$HELP" 1>&2 + exit 0 + ;; + d ) + DRAFT=true + ;; + m ) + FILES=("$REPO_README") + # release should be completed, so no longer prep + COMMIT_MESSAGE="πŸ₯‘πŸ€– $LATEST_TAG release πŸ€–πŸ₯‘" + PR_TITLE="πŸ₯‘πŸ€– $LATEST_TAG release" + RELEASE_PREP=false + ;; + r ) + # todo: validate $REPO_FULL_NAME + REPO_FULL_NAME="${OPTARG}" + ;; + \? ) + echo "$HELP" 1>&2 + exit 0 + ;; + esac + done + + # set repo full name to the default value if unset + if [ -z "$REPO_FULL_NAME" ]; then + REPO_FULL_NAME=$DEFAULT_REPO_FULL_NAME + fi +} + +# output formatting +export TERM="xterm" +RED=$(tput setaf 1) +MAGENTA=$(tput setaf 5) +RESET_FMT=$(tput sgr 0) +BOLD=$(tput bold) + +# verify origin tracking before creating and pushing new branches +verify_origin_tracking() { + origin=$(git remote get-url origin 2>&1) || true + + if [[ $origin == "fatal: No such remote 'origin'" ]] || [[ $origin == "https://github.com/awslabs/aws-simple-ec2-cli.git" ]]; then + echo -e "❌ ${RED}Expected remote 'origin' to be tracking fork but found \"$origin\". Set it up before running this script again.${RESET_FMT}" + NEED_ROLLBACK=false + exit 1 + fi +} + +create_release_branch() { + exists=$(git checkout -b "$NEW_BRANCH" 2>&1) || true + + if [[ $exists == "fatal: A branch named '$NEW_BRANCH' already exists." ]]; then + echo -e "❌ ${RED}$exists${RESET_FMT}" + NEED_ROLLBACK=false + exit 1 + fi + echo -e "βœ… ${BOLD}Created new release branch $NEW_BRANCH\n\n${RESET_FMT}" +} + +update_versions() { + # update release version for release prep + echo -e "πŸ₯‘ Attempting to update Simple-EC2 release version in preparation for a new release." + + for f in "${FILES[@]}"; do + # do not exit if grep doesn't find $PREVIOUS_VERSION; continue to exit on all other exit codes + has_incorrect_version=$(cat $f | grep "$PREVIOUS_VERSION" || [[ $? == 1 ]]) + if [[ ! -z $has_incorrect_version ]]; then + if [[ "${os}" = "Linux" ]]; then + sed -i "s/$PREVIOUS_VERSION/$LATEST_VERSION/g" $f + elif [[ "${os}" = "Darwin" ]]; then + sed -i '' "s/$PREVIOUS_VERSION/$LATEST_VERSION/g" $f + else + echo -e "❌ ${RED}Platform ${os} does not support Simple-EC2 release. Please use: Linux or macOS ${RESET_FMT}" + exit 1 + fi + FILES_CHANGED+=("$f") + fi + done + + if [[ ${#FILES_CHANGED[@]} -eq 0 ]]; then + echo -e "\nNo files were modified. Either all files already use git the latest release version $LATEST_VERSION or the files don't currently have the previous version $PREVIOUS_VERSION." + exit 0 + else + echo -e "βœ…βœ… ${BOLD}Updated versions from $PREVIOUS_VERSION to $LATEST_VERSION in files: \n$(echo "${FILES_CHANGED[@]}" | tr ' ' '\n')" + echo -e "To see changes, run \`git diff HEAD^ HEAD\`${RESET_FMT}" + fi + echo +} + +commit_changes() { + echo -e "\nπŸ₯‘ Adding and committing release version changes." + git config user.name "$EC2_BOT_USER" + git config user.email "$EC2_BOT_EMAIL" + EC2_BOT_SET=true + git add "${FILES_CHANGED[@]}" + git commit -m "$COMMIT_MESSAGE" + echo -e "βœ…βœ…βœ… ${BOLD}Committed release prep changes to new branch $NEW_BRANCH with commit message '$COMMIT_MESSAGE'\n\n${RESET_FMT}" +} + +confirm_with_user_and_create_pr(){ + git checkout $NEW_BRANCH # checkout new branch before printing git diff + + echo -e "\nπŸ₯‘${BOLD}The following PR will be created:\n" + cat << EOM + PR draft mode: $DRAFT + PR target repository: $REPO_FULL_NAME + PR source branch: $NEW_BRANCH + PR target branch: $REPO_FULL_NAME/$PR_BASE + PR title: $PR_TITLE + PR body: $PR_BODY + PR labels: $PR_LABEL_1, $PR_LABEL_2 + Changes in $NEW_BRANCH: + ${MAGENTA}$(git diff HEAD^ HEAD)${RESET_FMT} +EOM + + # gh actions cannot respond to prompts + if [[ $RELEASE_PREP == true ]]; then + while true; do + read -p "πŸ₯‘${BOLD}Do you wish to create the release prep PR? Enter y/n " yn + case $yn in + [Yy]* ) create_pr; break;; + [Nn]* ) rollback; exit;; + * ) echo "πŸ₯‘Please answer yes or no.";; + esac + done + else + create_pr + fi + echo "${RESET_FMT}" +} + +create_pr() { + git push -u origin $NEW_BRANCH # sets source branch for PR to NEW_BRANCH on the fork or origin + git checkout $NEW_BRANCH # checkout new branch before creating a pr + + if [[ $DRAFT == true ]]; then + gh pr create \ + --repo "$REPO_FULL_NAME" \ + --base "$PR_BASE" \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --label "$PR_LABEL_1" --label "$PR_LABEL_2" \ + --draft + else + gh pr create \ + --repo "$REPO_FULL_NAME" \ + --base "$PR_BASE" \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --label "$PR_LABEL_1" --label "$PR_LABEL_2" + fi + + if [[ $? == 0 ]]; then + echo -e "βœ…βœ…βœ…βœ… ${BOLD}Created $LATEST_TAG release prep PR\n${RESET_FMT}" + else + echo -e "❌ ${RED}PR creation failed.${RESET_FMT}❌" + exit 1 + fi +} + +# rollback partial changes to make this script atomic, iff the current execution of the script created a new branch and made changes +rollback() { + if [[ $NEED_ROLLBACK == true ]]; then + echo "πŸ₯‘${BOLD}Rolling back" + + # checkout of current branch to main + git checkout main + + # delete local and remote release branch only if current execution of the script created them + git branch -D $NEW_BRANCH + git push origin --delete $NEW_BRANCH + + # if multiple user.name are set, then only the latest(ec2-bot) will be removed + if [[ $EC2_BOT_SET == true ]]; then + echo "πŸ₯‘${BOLD}Rolling back ec2-bot git config" + git config --unset user.name + git config --unset user.email + fi + + fi + echo "${RESET_FMT}" +} + +handle_errors() { + # error handling + if [ $1 != "0" ]; then + FAILED_COMMAND=${*:2} + echo -e "\n❌ ${RED}Error occurred while running command '$FAILED_COMMAND'.${RESET_FMT}❌" + rollback + exit 1 + fi + exit $1 +} + +sync_local_tags_from_remote() { + # setup remote upstream tracking to fetch tags + git remote add the-real-upstream https://github.com/awslabs/aws-simple-ec2-cli.git &> /dev/null || true + git fetch the-real-upstream + + # delete all local tags + git tag -l | xargs git tag -d + + # fetch remote tags + git fetch the-real-upstream --tags + + # clean up tracking + git remote remove the-real-upstream +} + +main() { + process_args "$@" + trap 'handle_errors $? $BASH_COMMAND' EXIT + + verify_origin_tracking + sync_local_tags_from_remote + + # if previous and latest version are equal, then the previous previous release versions may be present + if [[ $PREVIOUS_VERSION == "$LATEST_VERSION" ]]; then + PREVIOUS_VERSION=$(git tag -l --sort=-v:refname | sed -n '2 p' | cut -b 2-) + fi + + echo -e "πŸ₯‘ Attempting to create a release prep branch and PR with release version updates.\n Previous version: $PREVIOUS_VERSION ---> Latest version: $LATEST_VERSION" + create_release_branch + update_versions + commit_changes + confirm_with_user_and_create_pr +} + +main "$@" \ No newline at end of file