summaryrefslogtreecommitdiff
path: root/devscripts/release.sh
diff options
context:
space:
mode:
authorDennis Fink2026-05-15 20:34:05 +0200
committerDennis Fink2026-05-15 20:43:03 +0200
commit80f2eb363063683a3e06b9b8c5da1b0835269eaa (patch)
tree3be751309f0695475ead4a67a08f647aa35edf22 /devscripts/release.sh
parent00dfe706231efe0b2296c93533c154e9957b2bfd (diff)
downloadtranscode.sh-80f2eb363063683a3e06b9b8c5da1b0835269eaa.tar.gz
transcode.sh-80f2eb363063683a3e06b9b8c5da1b0835269eaa.zip
chore: add release automation
Add a dedicated release helper so version and revision metadata can be prepared consistently before publishing a new release. Use git attributes to keep developer tooling and repository metadata out of generated source archives. Also ignore the dist directory created during the release process.
Diffstat (limited to 'devscripts/release.sh')
-rwxr-xr-xdevscripts/release.sh679
1 files changed, 679 insertions, 0 deletions
diff --git a/devscripts/release.sh b/devscripts/release.sh
new file mode 100755
index 0000000..d95adb3
--- /dev/null
+++ b/devscripts/release.sh
@@ -0,0 +1,679 @@
+#!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+###############################################################################
+# DESCRIPTION
+# Release helper for transcode.sh.
+#
+# This developer script prepares and publishes project releases. The prepare
+# command updates release metadata in the main script, man page, and README
+# markers. The release command verifies that metadata, commits the release,
+# creates an annotated git tag, and writes the source archive to dist/.
+#
+# NOTES
+# This script respects the NO_COLOR standard (https://no-color.org/).
+#
+# AUTHOR
+# Dennis Fink <me+coding@dennisfink.me>
+###############################################################################
+
+# TRACE enables bash execution tracing and is intended for debugging control
+# flow / expansions. DEBUG enables structured debug messages (see debug()) and
+# is intended for high-level state reporting.
+#
+# Recommended usage:
+# TRACE=1 scripts/release.sh ... # show every command bash executes
+# DEBUG=1 scripts/release.sh ... # show script-defined debug messages
+
+TRACE=${TRACE:-0}
+if [[ $TRACE -eq 1 ]]; then
+ set -o xtrace
+fi
+
+###############################################################################
+# SHELL SCRIPT HARDENING
+#
+# This script enables strict and predictable behavior for improved safety,
+# security, and debuggability. These measures help avoid common pitfalls
+# such as accidental globbing, unexpected alias expansion, silent failures,
+# or unintended word splitting.
+###############################################################################
+
+# Unalias everything to avoid unexpected alias expansion
+command unalias -a
+
+# Clear the shell command hash table to avoid stale command lookups
+hash -r
+
+# Set a safe IFS to prevent word-splitting vulnerabilities
+IFS=$'\n\t'
+
+# Set a secure default file creation mask (controls default permissions)
+umask 002
+
+# Ensure ERR traps are inherited by functions and subshells
+set -o errtrace
+# Treat use of undefined variables as an error and exit
+set -o nounset
+# Make pipelines fail if any command in the pipeline fails
+set -o pipefail
+
+###############################################################################
+# SCRIPT METADATA
+#
+# These variables describe the script and are used for the --version output
+# and for informational messages. They are marked readonly to prevent
+# accidental modification at runtime.
+###############################################################################
+readonly SCRIPTNAME=${0##*/}
+readonly DESCRIPTION="Release helper for transcode.sh."
+readonly DATE_OF_CREATION=2026-05-15
+readonly DATE_OF_REVISION=2026-05-15
+readonly VERSION=0.0.1
+readonly AUTHOR="Dennis Fink <me+coding@dennisfink.me>"
+readonly LICENSE="BSD-3-Clause"
+
+###############################################################################
+# GLOBAL VARIABLES
+###############################################################################
+
+ALL_OFF=""
+BOLD=""
+RED=""
+GREEN=""
+YELLOW=""
+BLUE=""
+MAGENTA=""
+CYAN=""
+
+DEBUG=${DEBUG:-0}
+DRY_RUN=0
+QUIET=0
+VERBOSE=0
+
+COMMAND=""
+RELEASE_VERSION=""
+
+readonly DIST_DIR="dist"
+readonly PROJECT_NAME="transcode-sh"
+readonly MAIN_SCRIPT="transcode.sh"
+readonly MAN_PAGE="transcode.sh.1"
+readonly README_FILE="README.md"
+
+###############################################################################
+# EXIT CODES
+###############################################################################
+
+EXIT_OK=0
+EXIT_RUNTIME_FAILURE=1
+EXIT_USAGE_ERROR=2
+EXIT_CONFIG_ERROR=3
+EXIT_MISSING_DEPENDENCY=127
+readonly EXIT_OK EXIT_RUNTIME_FAILURE EXIT_USAGE_ERROR EXIT_CONFIG_ERROR EXIT_MISSING_DEPENDENCY
+
+##############################################################################
+# FUNCTIONS
+#
+# This section defines the helper functions used throughout the script. They
+# handle formatted output, error reporting, version information, utility
+# functions and runtime behaviour such as displaying usage instructions.
+###############################################################################
+
+emit() {
+ local prefix="$1"
+ local color="$2"
+ shift 2
+
+ if [[ $# -eq 0 ]]; then
+ printf "%b%s%b\n" "${BOLD}${color}" "$prefix" "$ALL_OFF"
+ return
+ fi
+
+ local mesg="$1"
+ shift
+
+ local rest=""
+ if [[ $# -gt 0 ]]; then
+ printf -v rest ' %s' "$@"
+ rest="${rest# }"
+ fi
+
+ printf "%b%s%b %b%s%b" "${BOLD}${color}" "$prefix" "$ALL_OFF" "$BOLD" "$mesg" "$ALL_OFF"
+ if [[ -n "$rest" ]]; then
+ printf " %s" "$rest"
+ fi
+ printf "\n"
+}
+
+error() {
+ emit "==> ERROR:" "$RED" "$@" >&2
+}
+
+msg() {
+ if [[ $QUIET -eq 0 ]]; then
+ emit "==>" "$GREEN" "$@"
+ fi
+}
+
+warn() {
+ if [[ $QUIET -eq 0 ]]; then
+ emit "==>" "$YELLOW" "$@"
+ fi
+}
+
+verbose() {
+ if [[ $VERBOSE -eq 1 && $QUIET -eq 0 ]]; then
+ emit "==>" "$BLUE" "$@"
+ fi
+}
+
+debug() {
+ if [[ $DEBUG -eq 1 ]]; then
+ emit "==> DEBUG:" "$MAGENTA" "$@"
+ fi
+}
+
+print_help() {
+ printf "${BOLD}${MAGENTA}%s${ALL_OFF} - ${BOLD}${CYAN}%s${ALL_OFF} - ${BOLD}%s${ALL_OFF}
+
+${BOLD}${BLUE}Usage:${ALL_OFF} ${BOLD}${MAGENTA}%s${ALL_OFF} ${BOLD}${YELLOW}[OPTION]...${ALL_OFF} ${BOLD}${GREEN}COMMAND VERSION${ALL_OFF}
+
+${BOLD}${BLUE}Commands:${ALL_OFF}
+ ${BOLD}${GREEN}prepare${ALL_OFF} ${BOLD}${CYAN}VERSION${ALL_OFF} update release metadata and show the resulting diff
+ ${BOLD}${GREEN}release${ALL_OFF} ${BOLD}${CYAN}VERSION${ALL_OFF} verify metadata, commit, tag, and create archive
+
+${BOLD}${BLUE}Release options:${ALL_OFF}
+ ${BOLD}${YELLOW}-n, --dry-run${ALL_OFF} show what would be done without changing files, commits, tags, or archives
+
+${BOLD}${BLUE}Output options:${ALL_OFF}
+ ${BOLD}${YELLOW}-q, --quiet${ALL_OFF} suppress informational output
+ ${BOLD}${YELLOW}-v, --verbose${ALL_OFF} explain what is being done
+ ${BOLD}${YELLOW}--color${ALL_OFF} force colored output
+ ${BOLD}${YELLOW}--no-color${ALL_OFF} disable colored output
+
+${BOLD}${BLUE}General options:${ALL_OFF}
+ ${BOLD}${YELLOW}-h, --help${ALL_OFF} display this help and exit
+ ${BOLD}${YELLOW}--version${ALL_OFF} output version information and exit
+
+${BOLD}${BLUE}Examples:${ALL_OFF}
+ %s prepare 1.1.0
+ %s --dry-run prepare 1.1.0
+ %s release 1.1.0
+
+${BOLD}${BLUE}Notes:${ALL_OFF}
+ ${BOLD}${GREEN}prepare${ALL_OFF} must be run before ${BOLD}${GREEN}release${ALL_OFF}.
+ Archives are written below ${BOLD}${CYAN}%s/${ALL_OFF}.
+" \
+ "$SCRIPTNAME" "$VERSION" "$DESCRIPTION" \
+ "$SCRIPTNAME" \
+ "$SCRIPTNAME" "$SCRIPTNAME" "$SCRIPTNAME" "$DIST_DIR"
+}
+
+print_version() {
+ printf "${RED}${BOLD}Scriptname:${ALL_OFF} %s
+${GREEN}${BOLD}Version:${ALL_OFF} %s
+${YELLOW}${BOLD}Description:${ALL_OFF} %s
+${BLUE}${BOLD}Author:${ALL_OFF} %s
+${MAGENTA}${BOLD}Date of creation:${ALL_OFF} %s
+${CYAN}${BOLD}Date of revision:${ALL_OFF} %s
+${RED}${BOLD}License:${ALL_OFF} %s
+
+Copyright (c) 2026 Dennis Fink <me+coding@dennisfink.me>.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" \
+ "$SCRIPTNAME" "$VERSION" "$DESCRIPTION" "$AUTHOR" "$DATE_OF_CREATION" \
+ "$DATE_OF_REVISION" "$LICENSE"
+}
+
+###############################################################################
+# TERMINAL COLOR SETUP
+#
+# Colors and style escape sequences are configured dynamically through the
+# `setup_colors` function, which enables or disables color output based on
+# the value of ENABLE_COLOR. When enabled, terminal capabilities are detected
+# via `tput`, falling back to ANSI escapes if unavailable.
+#
+# This script respects the NO_COLOR standard (https://no-color.org/).
+# If the environment variable NO_COLOR is set (to any value), all color output
+# is disabled. If FORCE_COLOR is set, colors are always enabled.
+###############################################################################
+
+# NOTE: This block calls debug() and reads $DEBUG. Both must be defined before
+# this point. Do not move this block above the GLOBAL VARIABLES or FUNCTIONS
+# sections without adjusting those dependencies.
+if [[ -n "${NO_COLOR:-}" ]]; then
+ debug "Colors are disabled via NO_COLOR"
+ ENABLE_COLOR=0
+elif [[ -n "${FORCE_COLOR:-}" ]]; then
+ debug "Colors are forced via FORCE_COLOR"
+ ENABLE_COLOR=1
+elif [[ -t 1 ]]; then
+ debug "Colors are enabled via interactive terminal"
+ ENABLE_COLOR=1
+else
+ debug "Colors are disabled"
+ ENABLE_COLOR=0
+fi
+
+setup_colors() {
+ if [[ "$ENABLE_COLOR" -eq 1 ]]; then
+ if tput setaf 0 >/dev/null 2>&1; then
+ debug "Setting colors via tput"
+ ALL_OFF="$(tput sgr0)"
+ BOLD="$(tput bold)"
+ RED="$(tput setaf 1)"
+ GREEN="$(tput setaf 2)"
+ YELLOW="$(tput setaf 3)"
+ BLUE="$(tput setaf 4)"
+ MAGENTA="$(tput setaf 5)"
+ CYAN="$(tput setaf 6)"
+ else
+ # Hardcoded ANSI fallback: colors only (no bold embedded), matching the
+ # tput path where BOLD and color variables are kept separate.
+ debug "Using hardcoded color ANSI escape sequences"
+ ALL_OFF="\e[0m"
+ BOLD="\e[1m"
+ RED="\e[31m"
+ GREEN="\e[32m"
+ YELLOW="\e[33m"
+ BLUE="\e[34m"
+ MAGENTA="\e[35m"
+ CYAN="\e[36m"
+ fi
+ else
+ ALL_OFF=""
+ BOLD=""
+ RED=""
+ GREEN=""
+ YELLOW=""
+ BLUE=""
+ MAGENTA=""
+ CYAN=""
+ fi
+}
+setup_colors
+
+###############################################################################
+# RELEASE HELPERS
+###############################################################################
+
+require_command() {
+ local command_name="$1"
+
+ command -v "$command_name" >/dev/null 2>&1 || {
+ error "$command_name not found."
+ exit $EXIT_MISSING_DEPENDENCY
+ }
+}
+
+require_release_dependencies() {
+ require_command date
+ require_command git
+ require_command grep
+ require_command sed
+ require_command tar
+}
+
+validate_version() {
+ local version="$1"
+
+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ error "Version must use SemVer format: MAJOR.MINOR.PATCH" "$version"
+ exit $EXIT_USAGE_ERROR
+ fi
+}
+
+require_project_files() {
+ local file
+
+ for file in "$MAIN_SCRIPT" "$MAN_PAGE" "$README_FILE"; do
+ [[ -f "$file" ]] || {
+ error "Required project file not found:" "$file"
+ exit $EXIT_CONFIG_ERROR
+ }
+ done
+}
+
+require_clean_worktree() {
+ if ! git diff --quiet --; then
+ error "working tree has unstaged changes"
+ exit $EXIT_RUNTIME_FAILURE
+ fi
+
+ if ! git diff --cached --quiet --; then
+ error "index has staged changes"
+ exit $EXIT_RUNTIME_FAILURE
+ fi
+}
+
+require_tag_absent() {
+ local version="$1"
+
+ if git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then
+ error "tag already exists:" "v$version"
+ exit $EXIT_RUNTIME_FAILURE
+ fi
+}
+
+# Replace a literal release metadata pattern in a file.
+# Keep all sed -i portability handling in this one function.
+replace_literal() {
+ local file="$1"
+ local pattern="$2"
+ local replacement="$3"
+
+ local expression="s|$pattern|$replacement|"
+
+ debug "Replacing in $file:" "$pattern -> $replacement"
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ local matching_lines replaced_lines
+
+ matching_lines="$(grep -E -n -- "$pattern" "$file" || true)"
+
+ if [[ -z "$matching_lines" ]]; then
+ warn "DRY RUN:" "No matching line found in $file for pattern: $pattern"
+ return $EXIT_OK
+ fi
+
+ replaced_lines="$(printf "%s\n" "$matching_lines" | sed -E "$expression")"
+
+ msg "DRY RUN:" "Would replace in $file"
+ printf "%bCurrent:%b\n%s\n" "$BOLD" "$ALL_OFF" "$matching_lines"
+ printf "%bUpdated:%b\n%s\n" "$BOLD" "$ALL_OFF" "$replaced_lines"
+ return $EXIT_OK
+ fi
+
+ if sed --version >/dev/null 2>&1; then
+ # GNU sed
+ sed -i -E "$expression" "$file"
+ else
+ # BSD/macOS sed
+ sed -i '' -E "$expression" "$file"
+ fi
+}
+
+update_version_and_date() {
+ local version="$1"
+ local revision_date="$2"
+
+ msg "Updating release metadata:" "$version ($revision_date)"
+
+ replace_literal \
+ "$MAIN_SCRIPT" \
+ 'readonly VERSION=[0-9]+\.[0-9]+\.[0-9]+' \
+ "readonly VERSION=$version"
+
+ replace_literal \
+ "$MAIN_SCRIPT" \
+ 'readonly DATE_OF_REVISION=[0-9]{4}-[0-9]{2}-[0-9]{2}' \
+ "readonly DATE_OF_REVISION=$revision_date"
+
+ replace_literal \
+ "$MAN_PAGE" \
+ '\.TH TRANSCODE\.SH 1 "[0-9]{4}-[0-9]{2}-[0-9]{2}" "[0-9]+\.[0-9]+\.[0-9]+"' \
+ ".TH TRANSCODE.SH 1 \"$revision_date\" \"$version\""
+
+ if grep -q '<!-- release-version -->' "$README_FILE"; then
+ replace_literal \
+ "$README_FILE" \
+ '<!-- release-version -->.*<!-- /release-version -->' \
+ "<!-- release-version -->$version<!-- /release-version -->"
+ else
+ verbose "README release-version marker not found; skipping README version update"
+ fi
+
+ if grep -q '<!-- revision-date -->' "$README_FILE"; then
+ replace_literal \
+ "$README_FILE" \
+ '<!-- revision-date -->.*<!-- /revision-date -->' \
+ "<!-- revision-date -->$revision_date<!-- /revision-date -->"
+ else
+ verbose "README revision-date marker not found; skipping README date update"
+ fi
+}
+
+verify_release_metadata() {
+ local version="$1"
+ local revision_date="$2"
+
+ msg "Verifying release metadata:" "$version ($revision_date)"
+
+ grep -q "readonly VERSION=$version" "$MAIN_SCRIPT" || {
+ error "Version metadata mismatch in:" "$MAIN_SCRIPT"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+
+ grep -q "readonly DATE_OF_REVISION=$revision_date" "$MAIN_SCRIPT" || {
+ error "Revision date metadata mismatch in:" "$MAIN_SCRIPT"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+
+ grep -q ".TH TRANSCODE.SH 1 \"$revision_date\" \"$version\"" "$MAN_PAGE" || {
+ error "Release metadata mismatch in:" "$MAN_PAGE"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+
+ "./$MAIN_SCRIPT" --version | grep -q "Version:" || {
+ error "Could not read version output from:" "./$MAIN_SCRIPT"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+
+ "./$MAIN_SCRIPT" --version | grep -q "$version" || {
+ error "Version output does not contain release version:" "$version"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+
+ "./$MAIN_SCRIPT" --version | grep -q "$revision_date" || {
+ error "Version output does not contain revision date:" "$revision_date"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+}
+
+read_revision_date() {
+ sed -n -E 's/^readonly DATE_OF_REVISION=([0-9]{4}-[0-9]{2}-[0-9]{2})$/\1/p' "$MAIN_SCRIPT"
+}
+
+show_release_diff() {
+ msg "Release metadata diff:"
+ git diff -- "$MAIN_SCRIPT" "$MAN_PAGE" "$README_FILE"
+}
+
+commit_release() {
+ local version="$1"
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ msg "DRY RUN:" "Would create relase commit: chore(release): prepare $version"
+ return $EXIT_OK
+ fi
+
+ msg "Creating release commit:" "$version"
+ git add "$MAIN_SCRIPT" "$MAN_PAGE" "$README_FILE"
+ git commit -m "chore(release): prepare $version"
+}
+
+tag_release() {
+ local version="$1"
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ msg "DRY RUN:" "Would create release tag: v$version"
+ return $EXIT_OK
+ fi
+
+ msg "Creating release tag:" "v$version"
+ git tag -a "v$version" -m "Release v$version"
+}
+
+create_archive() {
+ local version="$1"
+ local archive_name="$PROJECT_NAME-$version.tar.gz"
+ local archive_path="$DIST_DIR/$archive_name"
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ msg "DRY RUN:" "Would create archive: $archive_path"
+ return $EXIT_OK
+ fi
+
+ mkdir -p -- "$DIST_DIR"
+
+ msg "Creating archive:" "$archive_path"
+ git archive \
+ --format=tar.gz \
+ --prefix="$PROJECT_NAME-$version/" \
+ --worktree-attributes \
+ -o "$archive_path" \
+ "v$version"
+
+ msg "Created:" "$archive_path"
+ verbose "Archive contents:"
+ if [[ $VERBOSE -eq 1 && $QUIET -eq 0 ]]; then
+ tar -tzf "$archive_path"
+ fi
+}
+
+prepare_release() {
+ local version="$1"
+ local revision_date
+ revision_date="$(date +%F)"
+
+ require_clean_worktree
+ update_version_and_date "$version" "$revision_date"
+
+ if [[ $DRY_RUN -eq 1 ]]; then
+ msg "DRY RUN:" "Would verify release metadata after applying changes"
+ msg "DRY RUN:" "Would show release metadata diff"
+ return $EXIT_OK
+ fi
+
+ verify_release_metadata "$version" "$revision_date"
+ show_release_diff
+}
+
+release() {
+ local version="$1"
+ local revision_date
+
+ revision_date="$(read_revision_date)"
+ [[ -n "$revision_date" ]] || {
+ error "Could not determine revision date from:" "$MAIN_SCRIPT"
+ exit $EXIT_RUNTIME_FAILURE
+ }
+
+ verify_release_metadata "$version" "$revision_date"
+ require_tag_absent "$version"
+ commit_release "$version"
+ tag_release "$version"
+ create_archive "$version"
+}
+
+###############################################################################
+# MAIN EXECUTION
+###############################################################################
+
+if [[ $# -eq 0 ]]; then
+ debug "No parameters were specified"
+ printf "${BOLD}${BLUE}Usage:${ALL_OFF} ${BOLD}${MAGENTA}%s${ALL_OFF} ${BOLD}${YELLOW}[OPTION]...${ALL_OFF} ${BOLD}${GREEN}COMMAND VERSION${ALL_OFF}
+Run ${BOLD}${MAGENTA}%s${ALL_OFF} ${BOLD}${YELLOW}--help${ALL_OFF} for full usage information.\n" "$SCRIPTNAME" "$SCRIPTNAME" >&2
+ exit $EXIT_USAGE_ERROR
+fi
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ -h | --help | -\?)
+ print_help
+ exit $EXIT_OK
+ ;;
+ --version)
+ print_version
+ exit $EXIT_OK
+ ;;
+ -q | --quiet)
+ QUIET=1
+ shift
+ ;;
+ -v | --verbose)
+ VERBOSE=1
+ shift
+ ;;
+ --color)
+ ENABLE_COLOR=1
+ setup_colors
+ shift
+ ;;
+ --no-color)
+ ENABLE_COLOR=0
+ setup_colors
+ shift
+ ;;
+ -n | --dry-run)
+ DRY_RUN=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ error "Unknown option:" "$1"
+ exit $EXIT_USAGE_ERROR
+ ;;
+ *)
+ break
+ ;;
+ esac
+done
+
+if [[ $# -ne 2 ]]; then
+ error "Expected COMMAND and VERSION."
+ print_help >&2
+ exit $EXIT_USAGE_ERROR
+fi
+
+COMMAND="$1"
+RELEASE_VERSION="$2"
+
+validate_version "$RELEASE_VERSION"
+require_release_dependencies
+require_project_files
+
+case "$COMMAND" in
+prepare)
+ prepare_release "$RELEASE_VERSION"
+ ;;
+release)
+ release "$RELEASE_VERSION"
+ ;;
+*)
+ error "Unknown command:" "$COMMAND"
+ print_help >&2
+ exit $EXIT_USAGE_ERROR
+ ;;
+esac
+
+exit $EXIT_OK