diff options
| author | Dennis Fink | 2026-05-15 20:34:05 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-05-15 20:43:03 +0200 |
| commit | 80f2eb363063683a3e06b9b8c5da1b0835269eaa (patch) | |
| tree | 3be751309f0695475ead4a67a08f647aa35edf22 | |
| parent | 00dfe706231efe0b2296c93533c154e9957b2bfd (diff) | |
| download | transcode.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.
| -rw-r--r-- | .gitattributes | 8 | ||||
| -rw-r--r-- | .gitignore | 2 | ||||
| -rwxr-xr-x | devscripts/release.sh | 679 | ||||
| -rw-r--r-- | justfile | 13 |
4 files changed, 702 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c1ed6dc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Exclude development/release tooling from source archives. +devscripts/ export-ignore +justfile export-ignore + +# Exclude Git/project metadata. +.gitignore export-ignore +.gitattributes export-ignore +.gitmessage export-ignore @@ -1,3 +1,5 @@ +dist/ + # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,zsh,fish,vim,emacs,git # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,zsh,fish,vim,emacs,git 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 diff --git a/justfile b/justfile new file mode 100644 index 0000000..7efa940 --- /dev/null +++ b/justfile @@ -0,0 +1,13 @@ +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] + +default: + just --list + +prepare version: + ./devscripts/release.sh prepare "{{version}}" + +release version: (prepare version) + ./devscripts/release.sh release "{{version}}" + +clean: + rm -f dist/transcode-*.tar.gz |
