diff --git a/Makefile b/Makefile index a7de431..49d9550 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ sa: ifndef STATIC_ANALYSIS_CHECKER @printf "\e[1m\e[31m%s\e[0m\n" "Shellcheck not installed: Static analysis not performed!" && exit 1 else - @shellcheck ./**/*.sh -C && printf "\e[1m\e[32m%s\e[0m\n" "ShellCheck: OK!" + @shellcheck ./**/**/*.sh -C && printf "\e[1m\e[32m%s\e[0m\n" "ShellCheck: OK!" endif lint: diff --git a/README.md b/README.md index 8c625c5..30ef6c0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,24 @@ Examples: ## Env variables +### RELEASE_SOURCE_BRANCH + +The default source branch that you want to use for your releases. + +> Default: `main` + +### RELEASE_TARGET_BRANCH + +The default target branch that you want to use for your releases. + +> Default: `prod` + +### RELEASE_DEVELOPMENT_BRANCH + +If you have a different develop branch from the source branch, you can also define it here. + +> Default: `main` + ### RELEASE_SUCCESSFUL_TEXT Display a text at the very end of the release. diff --git a/release b/release index b02b053..4ffa33a 100755 --- a/release +++ b/release @@ -12,6 +12,7 @@ source "$RELEASE_ROOT_DIR/src/colors.sh" source "$RELEASE_ROOT_DIR/src/compare.sh" source "$RELEASE_ROOT_DIR/src/console_header.sh" source "$RELEASE_ROOT_DIR/src/env.sh" +source "$RELEASE_ROOT_DIR/src/git.sh" source "$RELEASE_ROOT_DIR/src/io.sh" source "$RELEASE_ROOT_DIR/src/release.sh" source "$RELEASE_ROOT_DIR/src/validate.sh" @@ -19,18 +20,9 @@ source "$RELEASE_ROOT_DIR/src/slack.sh" source "$RELEASE_ROOT_DIR/src/json.sh" source "$RELEASE_ROOT_DIR/src/main.sh" -# Check if at least one argument (branch name) is passed -if [ $# -lt 1 ]; then - console_header::print_help - exit 1 -fi - -SOURCE_BRANCH=${1:-SOURCE_BRANCH:-"main"} -TARGET_BRANCH=${TARGET_BRANCH:-"prod"} -DEVELOPMENT_BRANCH=${DEVELOPMENT_BRANCH:-"main"} -RELEASE_SUCCESSFUL_TEXT=${RELEASE_SUCCESSFUL_TEXT:-} -DRY_RUN=${DRY_RUN:-false} +DRY_RUN=false FORCE_DEPLOY=false +GH_CLI_INSTALLED=false while [[ $# -gt 0 ]]; do argument="$1" @@ -53,33 +45,37 @@ while [[ $# -gt 0 ]]; do exit 0 ;; --source) - SOURCE_BRANCH="$2" + RELEASE_SOURCE_BRANCH="$2" shift ;; --target) - TARGET_BRANCH="$2" + RELEASE_TARGET_BRANCH="$2" shift ;; --develop) - DEVELOPMENT_BRANCH="$2" + RELEASE_DEVELOPMENT_BRANCH="$2" shift ;; + *) + RELEASE_SOURCE_BRANCH=$argument esac shift done -GH_CLI_INSTALLED=false if command -v gh &> /dev/null; then GH_CLI_INSTALLED=true fi -export GH_CLI_INSTALLED +####################################################### + export DRY_RUN +export FORCE_DEPLOY +export GH_CLI_INSTALLED +export RELEASE_SOURCE_BRANCH +export RELEASE_TARGET_BRANCH +export RELEASE_DEVELOPMENT_BRANCH -main::action "$SOURCE_BRANCH" \ - "$TARGET_BRANCH" \ - "$DEVELOPMENT_BRANCH" \ - "$FORCE_DEPLOY" +main::action "$FORCE_DEPLOY" echo -e "${COLOR_GREEN}Script completed${COLOR_RESET}" env::render_successful_text diff --git a/src/env.sh b/src/env.sh index 2b3a98f..9e16ce3 100644 --- a/src/env.sh +++ b/src/env.sh @@ -5,6 +5,16 @@ [[ -f ".env" ]] && source .env set set +o allexport +_DEFAULT_SOURCE_BRANCH="main" +_DEFAULT_TARGET_BRANCH="prod" +_DEFAULT_DEVELOPMENT_BRANCH="main" +_DEFAULT_SUCCESSFUL_TEXT="" + +: "${RELEASE_SOURCE_BRANCH:=${SOURCE_BRANCH:=$_DEFAULT_SOURCE_BRANCH}}" +: "${RELEASE_TARGET_BRANCH:=${TARGET_BRANCH:=$_DEFAULT_TARGET_BRANCH}}" +: "${RELEASE_DEVELOPMENT_BRANCH:=${DEVELOPMENT_BRANCH:=$_DEFAULT_DEVELOPMENT_BRANCH}}" +: "${RELEASE_SUCCESSFUL_TEXT:=${SUCCESSFUL_TEXT:=$_DEFAULT_SUCCESSFUL_TEXT}}" + function env::run_extra_confirmation() { local changed_files=$1 diff --git a/src/git.sh b/src/git.sh new file mode 100644 index 0000000..27dfc77 --- /dev/null +++ b/src/git.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +function git::status() { + git status +} + +function git::fetch_origin() { + git fetch origin +} + +function git::changed_files() { + git diff --name-only "$1".."$2" +} + +function git::latest_tag() { + git describe --tags "$(git rev-list --tags --max-count=1)" 2>/dev/null || echo "v0" +} + +function git::force_checkout() { + local branch_name="${1#origin/}" # Remove 'origin/' prefix if present + + if [[ "$DRY_RUN" == true ]]; then + echo -e "${COLOR_CYAN}--dry-run enabled. Skipping git fetch & checkout${COLOR_RESET}" \ + "${COLOR_ORANGE}origin/$branch_name${COLOR_RESET}" + return + fi + + git config advice.detachedHead false + [ -f .git/hooks/post-checkout ] && mv .git/hooks/post-checkout .git/hooks/post-checkout.bak + + git fetch origin + + if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then + # If branch exists locally, force checkout it + git checkout -f "$branch_name" + else + # If branch doesn't exist, create a new local branch from the remote + git checkout -b "$branch_name" origin/"$branch_name" + fi + + [ -f .git/hooks/post-checkout.bak ] && mv .git/hooks/post-checkout.bak .git/hooks/post-checkout + git config advice.detachedHead true +} + +function git::update_develop() { + local develop=$1 + local target=$2 + + echo -e "Merging ${COLOR_ORANGE}$target${COLOR_RESET} back to" \ + "${COLOR_ORANGE}$develop${COLOR_RESET} (increase the release contains hotfixes" \ + "that are not in ${COLOR_ORANGE}$develop${COLOR_RESET})" + + git::force_checkout "$develop" + git::merge_source_to_target "$target" "$develop" +} + +function git::merge_source_to_target() { + local source=$1 + local target=$2 + + echo -e "Merging ${COLOR_ORANGE}$source${COLOR_RESET} release to ${COLOR_ORANGE}$target${COLOR_RESET}" + + if [[ "$DRY_RUN" == true ]]; then + echo -e "${COLOR_CYAN}--dry-run enabled. Skipping git merge ($source into $target)${COLOR_RESET}" + return + fi + + if ! git merge "$source"; then + echo -e "${COLOR_RED}Merge failed. Please resolve conflicts and try again.${COLOR_RESET}" + exit 1 + fi + + git push origin "$target" --no-verify +} diff --git a/src/io.sh b/src/io.sh index de63e31..2b0f4e6 100644 --- a/src/io.sh +++ b/src/io.sh @@ -1,5 +1,4 @@ #!/bin/bash -set -euo pipefail # shellcheck disable=SC2155 function io::confirm_or_exit() { diff --git a/src/main.sh b/src/main.sh index f7f7cd7..87a109d 100644 --- a/src/main.sh +++ b/src/main.sh @@ -3,10 +3,11 @@ set -euo pipefail function main::action() { - local source=${1:-main} - local target=${2:-prod} - local develop=${3:-1:-main} - local force_release=${4:-false} + local force_release=${1:-false} + + local source=${RELEASE_SOURCE_BRANCH:-main} + local target=${RELEASE_TARGET_BRANCH:-prod} + local develop=${RELEASE_DEVELOPMENT_BRANCH:-$source} validate::slack_configured "$force_release" @@ -18,15 +19,15 @@ function main::action() { main::render_steps "$source" "$target" "$develop" echo -e "${COLOR_PURPLE}------------------------------------------------------------------${COLOR_RESET}" - git fetch origin - git status + git::fetch_origin + git::status echo -e "${COLOR_BLUE}------------------------------------------------------------------${COLOR_RESET}" echo -e "Using source branch: ${COLOR_ORANGE}$source${COLOR_RESET}" validate::no_diff_between_local_and_origin "$source" "$target" "$force_release" - local changed_files=$(git diff --name-only "$target".."$source") - local latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)" 2>/dev/null || echo "v0") + local changed_files=$(git::changed_files "$target" "$source") + local latest_tag=$(git::latest_tag) echo -e "Current latest tag: ${COLOR_CYAN}$latest_tag${COLOR_RESET}" local new_tag=$(release::generate_new_tag "$latest_tag" "$changed_files") @@ -45,19 +46,19 @@ function main::action() { env::run_extra_confirmation "$changed_files" env::run_extra_commands "$changed_files" - main::force_checkout "$target" - main::merge_source_to_target "$source" "$target" + git::force_checkout "$target" + git::merge_source_to_target "$source" "$target" release::create_tag "$target" "$new_tag" "$changed_files" release::create_github_release "$latest_tag" "$new_tag" - main::update_develop "$develop" "$target" + git::update_develop "$develop" "$target" } function main::render_steps() { local source=$1 - local target=$1 - local develop=$2 + local target=$2 + local develop=$3 echo "This script will automate the release process and follow the following steps:" echo "- Define the branch to release: $source" @@ -76,61 +77,3 @@ function main::render_steps() { return fi } - -function main::update_develop() { - local develop=$1 - local target=$2 - - echo -e "Merging ${COLOR_ORANGE}$target${COLOR_RESET} back to" \ - "${COLOR_ORANGE}$develop${COLOR_RESET} (increase the release contains hotfixes" \ - "that are not in ${COLOR_ORANGE}$develop${COLOR_RESET})" - - main::force_checkout "$develop" - main::merge_source_to_target "$target" "$develop" -} - -function main::merge_source_to_target() { - local source=$1 - local target=$2 - - echo -e "Merging ${COLOR_ORANGE}$source${COLOR_RESET} release to ${COLOR_ORANGE}$target${COLOR_RESET}" - - if [[ "$DRY_RUN" == true ]]; then - echo -e "${COLOR_CYAN}--dry-run enabled. Skipping git merge ($source into $target)${COLOR_RESET}" - return - fi - - if ! git merge "$source"; then - echo -e "${COLOR_RED}Merge failed. Please resolve conflicts and try again.${COLOR_RESET}" - exit 1 - fi - - git push origin "$target" --no-verify - -} - -function main::force_checkout() { - local branch_name="${1#origin/}" # Remove 'origin/' prefix if present - - if [[ "$DRY_RUN" == true ]]; then - echo -e "${COLOR_CYAN}--dry-run enabled. Skipping git fetch & checkout${COLOR_RESET}" \ - "${COLOR_ORANGE}origin/$branch_name${COLOR_RESET}" - return - fi - - git config advice.detachedHead false - [ -f .git/hooks/post-checkout ] && mv .git/hooks/post-checkout .git/hooks/post-checkout.bak - - git fetch origin - - if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then - # If branch exists locally, force checkout it - git checkout -f "$branch_name" - else - # If branch doesn't exist, create a new local branch from the remote - git checkout -b "$branch_name" origin/"$branch_name" - fi - - [ -f .git/hooks/post-checkout.bak ] && mv .git/hooks/post-checkout.bak .git/hooks/post-checkout - git config advice.detachedHead true -} diff --git a/tests/e2e/main_test.sh b/tests/e2e/main_test.sh index 3e81b58..1fd4b00 100644 --- a/tests/e2e/main_test.sh +++ b/tests/e2e/main_test.sh @@ -7,5 +7,61 @@ function set_up() { function test_main_without_args() { spy gh spy git - assert_match_snapshot "$($SCRIPT)" + assert_match_snapshot "$($SCRIPT -h)" +} + +function test_main_input_given_not_positive() { + skip "mocks are not ready yet to work on subshell..." && return + spy gh + spy read + export REPLY=n + + mock io::confirm_or_exit echo confirming_io + mock git::status echo "mocked git::status" + mock git::fetch_origin echo "mocked git::fetch_origin" + mock git::changed_files echo "mocked git::changed_files" + mock git::latest_tag echo "mocked git::latest_tag" + mock git::force_checkout echo "mocked git::force_checkout" + mock git::update_develop echo "mocked git::update_develop" + mock git::merge_source_to_target echo "mocked git::merge_source_to_target" + + assert_match_snapshot "$($SCRIPT --dry-run -f)" +} + +function test_main_no_changed_files() { + skip "mocks are not ready yet to work on subshell..." && return + + spy gh + spy read + export REPLY=y + + mock io::confirm_or_exit echo confirming_io + mock git::status echo "mocked git::status" + mock git::fetch_origin echo "mocked git::fetch_origin" + mock git::changed_files echo "mocked git::changed_files" + mock git::latest_tag echo "mocked git::latest_tag" + mock git::force_checkout echo "mocked git::force_checkout" + mock git::update_develop echo "mocked git::update_develop" + mock git::merge_source_to_target echo "mocked git::merge_source_to_target" + + assert_match_snapshot "$($SCRIPT --dry-run -f)" +} + +# idea: override bashunit mock - to write to /tmp/mocks.sh +function mock() { + local command=$1 + shift + + if [[ $# -gt 0 ]]; then + eval "function $command() { $*; }" + else + eval "function $command() { echo \"${CAT:-Mocked output}\"; }" + fi + + export -f "${command?}" + + # shellcheck disable=SC2005 + echo "$(declare -f "$command")" >> /tmp/mocks.sh + chmod +x /tmp/mocks.sh + trap 'rm -f /tmp/mocks.sh' EXIT }