Skip to content

Instantly share code, notes, and snippets.

@dazz
Created September 29, 2025 07:26
Show Gist options
  • Select an option

  • Save dazz/ce1d95b2c3d9ae6640f62c6d0bde2ee7 to your computer and use it in GitHub Desktop.

Select an option

Save dazz/ce1d95b2c3d9ae6640f62c6d0bde2ee7 to your computer and use it in GitHub Desktop.

Revisions

  1. dazz created this gist Sep 29, 2025.
    21 changes: 21 additions & 0 deletions Makefile
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    # Configuration
    RELEASE_BRANCH ?= main
    CALVER_PATTERN ?= %Y.%m.%d

    .PHONY: release release-auto release-next-tag release-check

    # Interactive release (prompts for pull and confirmation)
    release:
    @RELEASE_BRANCH=$(RELEASE_BRANCH) CALVER_PATTERN=$(CALVER_PATTERN) ./scripts/release.sh release

    # Automated release (no prompts, pull + create + push)
    release-auto:
    @RELEASE_BRANCH=$(RELEASE_BRANCH) CALVER_PATTERN=$(CALVER_PATTERN) ./scripts/release.sh release --auto

    # Show next tag
    release-next-tag:
    @RELEASE_BRANCH=$(RELEASE_BRANCH) CALVER_PATTERN=$(CALVER_PATTERN) ./scripts/release.sh next-tag

    # Check for changes
    release-check:
    @RELEASE_BRANCH=$(RELEASE_BRANCH) CALVER_PATTERN=$(CALVER_PATTERN) ./scripts/release.sh check-changes
    29 changes: 29 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,29 @@
    # Git Release Tag

    ## Usage

    ```bash
    # Interactive (default - asks for pull and confirmation)
    ./scripts/release.sh
    make release

    # Fully automated (no prompts at all)
    ./scripts/release.sh --auto
    ./scripts/release.sh release --auto
    make release-auto

    # Skip only confirmation
    ./scripts/release.sh --no-confirm

    # Skip only pull
    ./scripts/release.sh --no-pull

    # Kombinationen
    ./scripts/release.sh release --no-pull --no-confirm

    # Mit custom CalVer
    CALVER_PATTERN=%Y.%m ./scripts/release.sh --auto

    # Über Makefile
    make release-auto CALVER_PATTERN=%Y.%m
    ```
    268 changes: 268 additions & 0 deletions release.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,268 @@
    #!/bin/bash
    set -e

    # Configuration
    RELEASE_BRANCH="${RELEASE_BRANCH:-main}"
    CALVER_PATTERN="${CALVER_PATTERN:-%Y.%m.%d}"

    # Colors for output
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[1;33m'
    NC='\033[0m' # No Color

    # Functions

    show_help() {
    cat << EOF
    Usage: $(basename "$0") [COMMAND] [OPTIONS]
    Commands:
    release Create a new release (default)
    next-tag Show what the next tag would be
    check-branch Check if current branch is correct
    check-changes Check if there are new merges since last tag
    help Show this help message
    Release Options:
    --no-pull Skip git pull
    --no-confirm Skip confirmation prompt
    --auto Equivalent to --no-confirm (pull, create, push without prompts)
    Environment Variables:
    RELEASE_BRANCH Branch from which releases are created (default: main)
    CALVER_PATTERN Date format for CalVer tags (default: %Y.%m.%d)
    Examples:
    %Y.%m.%d -> 2025.09.29 (default)
    %Y.%m -> 2025.09
    %Y.%W -> 2025.39 (week number)
    v%Y.%m.%d -> v2025.09.29
    Examples:
    $(basename "$0") # Interactive release process
    $(basename "$0") release --auto # Automated release (no prompts)
    $(basename "$0") release --no-confirm # Pull interactively, skip confirmation
    $(basename "$0") release --no-pull # Skip pull, confirm before push
    $(basename "$0") next-tag # See what tag would be created
    CALVER_PATTERN=%Y.%m $(basename "$0") --auto # Automated with custom pattern
    EOF
    }

    check_branch() {
    local current_branch=$(git rev-parse --abbrev-ref HEAD)
    if [ "$current_branch" != "$RELEASE_BRANCH" ]; then
    echo -e "${RED}Error: Releases can only be created from the '$RELEASE_BRANCH' branch.${NC}"
    echo "Current branch: $current_branch"
    return 1
    fi
    echo -e "${GREEN}✓ On $RELEASE_BRANCH branch${NC}"
    return 0
    }

    get_last_tag() {
    git describe --tags --abbrev=0 2>/dev/null || echo ""
    }

    calculate_next_tag() {
    local base_tag=$(date +"$CALVER_PATTERN")
    local existing_tags=$(git tag -l "$base_tag*" | sort -V)

    if [ -z "$existing_tags" ]; then
    echo "$base_tag"
    else
    local last_today=$(echo "$existing_tags" | tail -n 1)
    if [ "$last_today" = "$base_tag" ]; then
    echo "$base_tag.1"
    else
    local suffix=$(echo "$last_today" | sed "s/$(echo "$base_tag" | sed 's/\./\\./g')\.//")
    local next_suffix=$((suffix + 1))
    echo "$base_tag.$next_suffix"
    fi
    fi
    }

    show_next_tag() {
    local last_tag=$(get_last_tag)
    local next_tag=$(calculate_next_tag)

    echo "Release branch: $RELEASE_BRANCH"
    echo "CalVer pattern: $CALVER_PATTERN"
    echo "Last tag: ${last_tag:-none}"
    echo "Next tag: $next_tag"
    }

    get_changelog() {
    local last_tag="$1"

    if [ -z "$last_tag" ]; then
    echo -e "${YELLOW}Warning: No previous tag found, showing all merge commits${NC}" >&2
    git log --merges HEAD \
    --pretty=format:"- %s" \
    | sed -E 's/^Merge (pull request #[0-9]+ from [^ ]+ |branch .+ )?//' \
    | head -n 20
    else
    git log --merges "$last_tag"..HEAD \
    --pretty=format:"- %s" \
    | sed -E 's/^Merge (pull request #[0-9]+ from [^ ]+ |branch .+ )?//'
    fi
    }

    check_changes() {
    local last_tag=$(get_last_tag)
    local msg=$(get_changelog "$last_tag")

    echo "Release branch: $RELEASE_BRANCH"
    echo "CalVer pattern: $CALVER_PATTERN"
    echo "Last tag: ${last_tag:-none}"
    echo ""

    if [ -z "$msg" ]; then
    echo -e "${YELLOW}No new merges since $last_tag${NC}"
    return 1
    else
    echo -e "${GREEN}Found new merges:${NC}"
    echo "$msg"
    return 0
    fi
    }

    update_branch() {
    local skip_pull="$1"

    if [ "$skip_pull" = "true" ]; then
    echo "Skipping git pull (--no-pull)"
    return 0
    fi

    read -p "Update $RELEASE_BRANCH branch (git pull)? [Y/n] " update_answer
    case $update_answer in
    [nN]*)
    echo "Branch will not be updated."
    ;;
    *)
    echo "Updating $RELEASE_BRANCH branch..."
    git pull
    ;;
    esac
    }

    create_release() {
    local skip_pull=false
    local skip_confirm=false

    # Parse arguments
    while [[ $# -gt 0 ]]; do
    case $1 in
    --no-pull)
    skip_pull=true
    shift
    ;;
    --no-confirm|--auto)
    skip_confirm=true
    shift
    ;;
    *)
    echo -e "${RED}Error: Unknown option '$1'${NC}"
    show_help
    exit 1
    ;;
    esac
    done

    # Check branch
    check_branch || exit 1

    # Update branch
    if [ "$skip_pull" = "false" ]; then
    if [ "$skip_confirm" = "true" ]; then
    echo "Updating $RELEASE_BRANCH branch..."
    git pull
    else
    update_branch "$skip_pull"
    fi
    else
    echo "Skipping git pull"
    fi

    # Get tags
    local last_tag=$(get_last_tag)
    local new_tag=$(calculate_next_tag)

    echo ""
    echo "CalVer pattern: $CALVER_PATTERN"
    echo "Last tag: ${last_tag:-none}"
    echo "New tag: $new_tag"

    # Generate changelog
    local msg=$(get_changelog "$last_tag")

    if [ -z "$msg" ]; then
    echo -e "${RED}No new merges since $last_tag${NC}"
    exit 1
    fi

    # Preview
    echo ""
    echo "Tag message preview:"
    echo "----------------------"
    echo "Release $new_tag"
    echo ""
    echo "$msg"
    echo "----------------------"
    echo ""

    # Confirmation
    if [ "$skip_confirm" = "true" ]; then
    echo "Creating and pushing tag automatically (--no-confirm)"
    git tag -a "$new_tag" -m "Release $new_tag"$'\n\n'"$msg"
    git push origin "$new_tag"
    echo -e "${GREEN}✓ Release $new_tag successfully created and pushed${NC}"
    else
    read -p "Really create and push tag? [y/N] " answer
    case $answer in
    [yY]*)
    git tag -a "$new_tag" -m "Release $new_tag"$'\n\n'"$msg"
    git push origin "$new_tag"
    echo -e "${GREEN}✓ Release $new_tag successfully created and pushed${NC}"
    ;;
    *)
    echo "Aborted."
    exit 1
    ;;
    esac
    fi
    }

    # Main script logic

    COMMAND="${1:-release}"

    case $COMMAND in
    release)
    shift # Remove 'release' from arguments
    create_release "$@"
    ;;
    next-tag)
    show_next_tag
    ;;
    check-branch)
    check_branch
    ;;
    check-changes)
    check_changes
    ;;
    help|--help|-h)
    show_help
    ;;
    --*)
    # If first argument is an option, assume 'release' command
    create_release "$@"
    ;;
    *)
    echo -e "${RED}Error: Unknown command '$COMMAND'${NC}"
    echo ""
    show_help
    exit 1
    ;;
    esac