From d0738b7289c2382b7942b45c94bc3a902c264a4a Mon Sep 17 00:00:00 2001 From: Ethan P Date: Thu, 15 Jun 2023 16:14:25 -0700 Subject: [PATCH] dev: Allow release script to be used as lib --- release.sh | 378 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 272 insertions(+), 106 deletions(-) diff --git a/release.sh b/release.sh index 6447044..4d36d1a 100755 --- a/release.sh +++ b/release.sh @@ -1,10 +1,256 @@ #!/usr/bin/env bash # ----------------------------------------------------------------------------- -# bat-extras | Copyright (C) 2019-2020 eth-p | MIT License +# bat-extras | Copyright (C) 2019-2023 eth-p | MIT License # # Repository: https://github.com/eth-p/bat-extras # Issues: https://github.com/eth-p/bat-extras/issues # ----------------------------------------------------------------------------- +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/build.sh" + +# ----------------------------------------------------------------------------- +# Release-as-a-Library Functions: +# ----------------------------------------------------------------------------- + +# Prints the path to the git workspace. +if ! batextras:is_function_defined ::get_git_workspace; then + batextras:get_git_workspace() { + batextras:lazy_done "${FUNCNAME[0]}" \ + "$(batextras:get_project_directory)" + } +fi + +# Prints the commit of the latest-tagged version of bat-extras. +if ! batextras:is_function_defined ::get_current_commit; then + batextras:get_current_commit() { + batextras:lazy_done "${FUNCNAME[0]}" \ + "$(batextras:git rev-parse HEAD)" + } +fi + +# Prints the commit of the latest-tagged version of bat-extras. +if ! batextras:is_function_defined ::get_previous_tag_commit; then + batextras:get_previous_tag_commit() { + local latest_tag + local before_latest_tag + { + read -r latest_tag + read -r before_latest_tag + } < <(batextras:git rev-list --tags --max-count=2) + + # If the latest commit is a tag, go to the one before that. + if [[ "$(batextras:get_current_commit)" = "$latest_tag" ]]; then + latest_tag="${before_latest_tag}" + fi + + batextras:lazy_done "${FUNCNAME[0]}" "$latest_tag" + } +fi + +# Prints the ref name of the latest-tagged version of bat-extras. +if ! batextras:is_function_defined ::get_previous_tag_name; then + batextras:get_previous_tag_name() { + batextras:lazy_done "${FUNCNAME[0]}" \ + "$(batextras:git describe --tags --abbrev=0 "$(batextras:get_previous_tag_commit)")" + } +fi + +# Returns the suffix for a day of the month. +# +# Arguments: +# 1 -- The day number. +# +# Output: +# The suffix. +batextras:day_suffix() { + case "$1" in + 11 | 12 | 13) echo "th" ;; + *1) echo "st" ;; + *2) echo "nd" ;; + *3) echo "rd" ;; + *) echo "th" ;; + esac +} + +# Runs `git` within the project directory. +# +# This takes the same arguments as git (with the exception of `-C`), and +# does exactly what `git` would normally do. +batextras:git() { + git -C "$(batextras:get_git_workspace)" "$@" + return $? +} + +# Creates the zipball for release. +# YOU MUST BUILD THE PROJECT FIRST! +# +# Arguments: +# 1 -- The absolute path to the output zip file. +# +# Stderr: +# Messages. +batextras:create_package() { + local artifact="$1" + local bin_dir man_dir doc_dir + bin_dir="$(batextras:get_output_bin_directory)" + man_dir="$(batextras:get_output_man_directory)" + doc_dir="$(batextras:get_docs_directory)" + + ( + # Remove the old zipball, if one exists. + if [[ -f "$artifact" ]]; then + rm "$artifact" || return $? + fi + + # Add the bin directory. + cd "$(dirname -- "$bin_dir")" || return $? + zip -r "$artifact" "$(basename -- "$bin_dir")" + + # Add the doc directory. + cd "$(dirname -- "$doc_dir")" || return $? + zip -ru "$artifact" "$(basename -- "$doc_dir")" + + # Add the man directory. + if [[ -d "$man_dir" ]]; then + cd "$(dirname -- "$man_dir")" || return $? + zip -ru "$artifact" "$(basename -- "$man_dir")" + fi + ) 1>&2 +} + +# Generates a Markdown changelog for all changes between two commits. +# +# Arguments: +# 1 -- The first commit, exclusive. +# 2 -- The second commit, inclusive. +# 3 -- A filter in regex. +# +# Output: +# The changelog. +batextras:generate_changelog() { + local start_commit="$1" + local end_commit="$2" + local filter="${3}" + + # Generate sed replacement patterns. + local script_links=() + local script_names=() + local script script_name + while read -r script; do + script_name="$(basename "$script" .sh)" + script_names+=("$script_name") + done < <(batextras:get_source_paths) + + local script_pattern + script_pattern="$(printf 's/\\(%s\\)/`\\1`/;' "${script_names[@]}")" + + # Generate the changelog. + local changelog='' + local commit + local affected_module + local commit_message + while read -r commit; do + commit_message="$(batextras:git show -s --format=%s "$commit")" + + if ! [[ "$commit_message" =~ ^([a-z-]+):[[:space:]]*(.*)$ ]]; then + continue + fi + + affected_module="${BASH_REMATCH[1]}" + + # Make module names consistent. + case "$affected_module" in + dev | lib | mdroff) affected_module="developer" ;; + tests) affected_module="test" ;; + doc) affected_module="docs" ;; + esac + + # Append to changelog. + if [[ "$affected_module" =~ ^($filter)$ ]]; then + changelog="$changelog"$'\n'" - ${commit_message}" + fi + done < <(batextras:git rev-list "${start_commit}..${end_commit}") + + # Print the changelog. + changelog="$(sed "$script_pattern" <<< "$changelog")" + printf "%s\n" "${changelog:1}" + return 0 +} + +# Generates the Markdown release notes. +# +# Arguments: +# 1 -- The oldest commit, exclusive. +# 2 -- The newest commit, inclusive. +# +# Output: +# The changelog. +batextras:generate_release_notes() { + local commit_oldest="$1" + local commit_newest="$2" + local commit_newest_url="https://github.com/eth-p/bat-extras/tree/${commit_newest}" + local date_str + + # Get the commit date. + local date_year date_month date_day date_month_text date_day_suffix + read -r date_year date_month date_day date_month_text \ + < <(batextras:git show -s --format="%cd" --date="format:%Y %m %d %B" "$commit_newest") + + date_day_suffix="$(batextras:day_suffix "$date_day")" + date_str="${date_month_text} ${date_day}${date_day_suffix}, ${date_year}" + + # For each built script, we want to: + # - Get the name of the script. + # - Get a link to the documentation. + # - Add it to the filter for non-developer items. + local script_name script_names script_links script_filters script_list_markdown + script_links=() + script_names=() + script_filters='' + + while read -r script; do + script_name="$(basename "$script" .sh)" + script_names+=("$script_name") + script_links+=("[\`${script_name}\`](https://github.com/eth-p/bat-extras/blob/${commit_newest}/doc/${script_name}.md)") + script_filters="${script_filters}|$(printf "%q" "$script_name")" + done < <(batextras:get_source_paths) + + script_filters="${script_filters:1}" # Remove the leading "|" + script_list_markdown="$(printf "%s, " "${script_links[@]:0:$((${#script_links[@]} - 1))}")" + script_list_markdown="${script_list_markdown}and ${script_links[$((${#script_links[@]} - 1))]}" + + # Get the changelog. + local changelog changelog_dev + changelog="$(batextras:generate_changelog "$commit_oldest" "$commit_newest" "$script_filters")" + changelog_dev="$(batextras:generate_changelog "$commit_oldest" "$commit_newest" "test|developer|ci|build")" + + # Print the template. + { sed '/\\$/{N;s/\\\n//;s/\n//p;}'; } <<- EOF + This contains the latest versions of ${script_list_markdown} as of commit [${commit_newest}](${commit_newest_url}) (${date_str}). + + **This is provided as a convenience only.** + I would still recommend following the installation instructions in [the README](https://github.com/eth-p/bat-extras#installation-) for the most up-to-date versions. + + ### Changes + ${changelog} + + ### Developer +
+
+ + ${changelog_dev} + +
+
+ EOF + +} + +# ----------------------------------------------------------------------------- +# Main: +# Only run everything past this point if the script is not sourced. +# ----------------------------------------------------------------------------- +(return 0 2>/dev/null) && return 0 + HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DATE="$(date +%Y%m%d)" VERSION="$(< "${HERE}/version.txt")" @@ -14,22 +260,23 @@ SRC="$HERE/src" source "${LIB}/print.sh" source "${LIB}/opt.sh" # ----------------------------------------------------------------------------- +set -euo pipefail # ----------------------------------------------------------------------------- # Options. OPT_ARTIFACT="bat-extras-${DATE}.zip" OPT_SINCE= OPT_BAD_IDEA=false -OPT_BIN_DIR="$HERE/bin" -OPT_DOC_DIR="$HERE/doc" -OPT_MAN_DIR="$HERE/man" +OPT_BIN_DIR="$(batextras:get_output_bin_directory)" +OPT_DOC_DIR="$(batextras:get_docs_directory)" +OPT_MAN_DIR="$(batextras:get_output_man_directory)" while shiftopt; do case "$OPT" in --since) shiftval OPT_SINCE="$OPT_VAL" - if ! git rev-parse "$OPT_SINCE" &> /dev/null; then + if ! batextras:git rev-parse "$OPT_SINCE" &> /dev/null; then printc "%{RED}%s: unknown commit or tag for '%s'\n" "$PROGRAM" "$OPT" exit 1 fi @@ -48,6 +295,10 @@ done # ----------------------------------------------------------------------------- # Verify the version matches today's date. + +VERSION="$(source "${LIB}/constants.sh" && echo "${PROGRAM_VERSION}")" +VERSION_EXPECTED="$(date +%Y.%m.%d)" + if [[ "$VERSION" != "$VERSION_EXPECTED" ]] && ! "$OPT_BAD_IDEA"; then printc "%{RED}The expected version does not match %{DEFAULT}version.txt%{RED}!%{CLEAR}\n" printc "%{RED}Expected: %{YELLOW}%s%{CLEAR}\n" "$VERSION_EXPECTED" @@ -55,6 +306,18 @@ if [[ "$VERSION" != "$VERSION_EXPECTED" ]] && ! "$OPT_BAD_IDEA"; then exit 1 fi +# ----------------------------------------------------------------------------- +# Verify the working tree is clean-ish. + +if ! "$OPT_BAD_IDEA"; then + while read -r flags file; do + if [[ "$flags" =~ M ]]; then + printc "%{RED}Found an uncommitted change in %{DEFAULT}%s%{RED}!%{CLEAR}\n" "$file" + exit 1 + fi + done < <(batextras:git status --porcelain) +fi + # ----------------------------------------------------------------------------- # Build files. @@ -75,110 +338,13 @@ printc "%{YELLOW}Building scripts...%{CLEAR}\n" # Build package. printc "%{YELLOW}Packaging artifacts...%{CLEAR}\n" -( - rm "$OPT_ARTIFACT" - cd "$(dirname "$OPT_BIN_DIR")" - zip -r "$OPT_ARTIFACT" "$(basename "$OPT_BIN_DIR")" - cd "$(dirname "$OPT_DOC_DIR")" - zip -ru "$OPT_ARTIFACT" "$(basename "$OPT_DOC_DIR")" - if [[ -d "$OPT_MAN_DIR" ]]; then - cd "$(dirname "$OPT_MAN_DIR")" - zip -ru "$OPT_ARTIFACT" "$(basename "$OPT_MAN_DIR")" - fi -) - +batextras:create_package "$OPT_ARTIFACT" printc "%{YELLOW}Package created as %{BLUE}%s%{YELLOW}.%{CLEAR}\n" "$OPT_ARTIFACT" # ----------------------------------------------------------------------------- # Print template description package. printc "%{YELLOW}Release description:%{CLEAR}\n" - -# Get the commit hash. -COMMIT="$(git rev-parse HEAD)" -COMMIT_URL="https://github.com/eth-p/bat-extras/tree/${COMMIT}" - -# Get the release date string. -DATE_DAY="$(date +%e | sed 's/ //')" -DATE_SUFFIX="" -case "$DATE_DAY" in - 11 | 12 | 13) DATE_SUFFIX="th" ;; - *1) DATE_SUFFIX="st" ;; - *2) DATE_SUFFIX="nd" ;; - *3) DATE_SUFFIX="rd" ;; - *) DATE_SUFFIX="th" ;; -esac -DATE_STR="$(date +'%B') ${DATE_DAY}${DATE_SUFFIX}, $(date +'%Y')" - -# Get the script names. -script_links=() -script_names=() -for script in "$SRC"/*.sh; do - script_name="$(basename "$script" .sh)" - script_names+=("$script_name") - script_links+=("[\`${script_name}\`](https://github.com/eth-p/bat-extras/blob/${COMMIT}/doc/${script_name}.md)") -done - -script_pattern="$(printf 's/\\(%s\\)/`\\1`/;' "${script_names[@]}")" -SCRIPTS="$(printf "%s, " "${script_links[@]:0:$((${#script_links[@]} - 1))}")" -SCRIPTS="${SCRIPTS}and ${script_links[$((${#script_links[@]} - 1))]}" - -# Get the changelog. -CHANGELOG_DEV='' -CHANGELOG='' -if [[ -n "$OPT_SINCE" ]]; then - ref="$(git rev-parse HEAD)" - end="$(git rev-parse "$OPT_SINCE")" - while [[ "$ref" != "$end" ]]; do - is_developer=false - ref_message="$(git show -s --format=%s "$ref")" - ref="$(git rev-parse "${ref}~1")" - - if [[ "$ref_message" =~ ^([a-z-]+):[[:space:]]*(.*)$ ]]; then - affected_module="${BASH_REMATCH[1]}" - - # Make module names consistent. - case "$affected_module" in - dev | lib | mdroff) affected_module="developer" ;; - tests) affected_module="test" ;; - doc) affected_module="docs" ;; - esac - - # Switch to the correct changelog. - case "$affected_module" in - test | developer | ci | build) is_developer=true ;; - esac - fi - - # Append to changelog. - if "$is_developer"; then - CHANGELOG_DEV="$CHANGELOG_DEV"$'\n'" - ${ref_message}" - else - CHANGELOG="$CHANGELOG"$'\n'" - ${ref_message}" - fi - done -fi - -CHANGELOG="$(sed "$script_pattern" <<< "$CHANGELOG")" -CHANGELOG_DEV="$(sed "$script_pattern" <<< "$CHANGELOG_DEV")" - -# Print the template. -sed '/\\$/{N;s/\\\n//;s/\n//p;}' <<- EOF - This contains the latest versions of $SCRIPTS as of commit [$(git rev-parse --short HEAD)]($COMMIT_URL) (${DATE_STR}). - - **This is provided as a convenience only.** - I would still recommend following the installation instructions in \ - [the README](https://github.com/eth-p/bat-extras#installation-) for the most up-to-date versions. - - ### Changes - $CHANGELOG - - ### Developer -
-
- - $CHANGELOG_DEV - -
-
-EOF +batextras:generate_release_notes \ + "$(batextras:get_previous_tag_name)" \ + "$(batextras:get_current_commit)"