diff --git a/.github/workflows/issue-stats.yml b/.github/workflows/issue-stats.yml index 8f49cf5..77c019c 100644 --- a/.github/workflows/issue-stats.yml +++ b/.github/workflows/issue-stats.yml @@ -1,3 +1,4 @@ +--- name: Monthly issue metrics on: workflow_dispatch: diff --git a/.serena/project.yml b/.serena/project.yml index b4d09d1..6c0c4e0 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -4,10 +4,6 @@ # * For JavaScript, use typescript # Special requirements: # * csharp: Requires the presence of a .sln file in the project folder. -language: bash - -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 ignore_all_files_in_gitignore: true # list of additional paths to ignore # same syntax as gitignore, so you can use * and ** @@ -66,3 +62,8 @@ excluded_tools: [] initial_prompt: '' project_name: 'actions' +languages: + - bash + - python +included_optional_tools: [] +encoding: utf-8 diff --git a/Makefile b/Makefile index d3a8dda..a470c88 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile for GitHub Actions repository # Provides organized task management with parallel execution capabilities -.PHONY: help all docs update-catalog lint format check clean install-tools test test-unit test-integration test-coverage generate-tests generate-tests-dry test-generate-tests docker-build docker-push docker-test docker-login docker-all release update-version-refs bump-major-version check-version-refs +.PHONY: help all docs update-catalog lint format check clean install-tools test test-unit test-integration test-coverage generate-tests generate-tests-dry test-generate-tests docker-build docker-push docker-test docker-login docker-all release release-dry release-prep release-tag release-undo update-version-refs bump-major-version check-version-refs .DEFAULT_GOAL := help # Colors for output @@ -159,12 +159,36 @@ fix-local-refs-dry: ## Preview local action reference fixes (dry run) release: ## Create a new release with version tags (usage: make release [VERSION=v2025.10.18]) @VERSION_TO_USE=$$(if [ -n "$(VERSION)" ]; then echo "$(VERSION)"; else date +v%Y.%m.%d; fi); \ echo "$(BLUE)šŸš€ Creating release $$VERSION_TO_USE...$(RESET)"; \ - sh _tools/release.sh "$$VERSION_TO_USE"; \ - echo "$(GREEN)āœ… Release created$(RESET)"; \ - echo ""; \ - echo "$(YELLOW)Next steps:$(RESET)"; \ - echo " 1. Review changes: git show HEAD"; \ - echo " 2. Push tags: git push origin main --tags --force-with-lease" + sh _tools/release.sh "$$VERSION_TO_USE" + +release-dry: ## Preview release without making changes (usage: make release-dry VERSION=v2025.11.01) + @if [ -z "$(VERSION)" ]; then \ + VERSION_TO_USE=$$(date +v%Y.%m.%d); \ + else \ + VERSION_TO_USE="$(VERSION)"; \ + fi; \ + echo "$(BLUE)šŸ” Previewing release $$VERSION_TO_USE (dry run)...$(RESET)"; \ + sh _tools/release.sh --dry-run "$$VERSION_TO_USE" + +release-prep: ## Update action refs and commit (no tags) (usage: make release-prep [VERSION=v2025.11.01]) + @VERSION_TO_USE=$$(if [ -n "$(VERSION)" ]; then echo "$(VERSION)"; else date +v%Y.%m.%d; fi); \ + echo "$(BLUE)šŸ”§ Preparing release $$VERSION_TO_USE...$(RESET)"; \ + sh _tools/release.sh --prep-only "$$VERSION_TO_USE"; \ + echo "$(GREEN)āœ… Preparation complete$(RESET)"; \ + echo "$(YELLOW)Next: make release-tag VERSION=$$VERSION_TO_USE$(RESET)" + +release-tag: ## Create tags only (assumes prep done) (usage: make release-tag VERSION=v2025.11.01) + @if [ -z "$(VERSION)" ]; then \ + echo "$(RED)āŒ Error: VERSION parameter required for release-tag$(RESET)"; \ + echo "Usage: make release-tag VERSION=v2025.11.01"; \ + exit 1; \ + fi; \ + echo "$(BLUE)šŸ·ļø Creating tags for release $(VERSION)...$(RESET)"; \ + sh _tools/release.sh --tag-only "$(VERSION)" + +release-undo: ## Rollback the most recent release (delete tags and reset HEAD) + @echo "$(BLUE)šŸ”™ Rolling back release...$(RESET)"; \ + sh _tools/release-undo.sh update-version-refs: ## Update all action references to a specific version tag (usage: make update-version-refs MAJOR=v2025) @if [ -z "$(MAJOR)" ]; then \ diff --git a/_tools/release-undo.sh b/_tools/release-undo.sh new file mode 100755 index 0000000..88568d3 --- /dev/null +++ b/_tools/release-undo.sh @@ -0,0 +1,152 @@ +#!/bin/sh +# Undo the most recent release by deleting tags and optionally resetting HEAD +set -eu + +# Source shared utilities +# shellcheck source=_tools/shared.sh +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/shared.sh" + +# Check git availability +require_git + +msg_info "Finding most recent release tags..." + +# Portable version sort function +# Sorts CalVer tags vYYYY.MM.DD numerically +version_sort_tags() { + # Try GNU sort first (Linux and some macOS with GNU coreutils) + if sort --version 2>/dev/null | grep -q GNU; then + sort -V + return + fi + + # Try gsort (macOS with GNU coreutils via Homebrew) + if command -v gsort >/dev/null 2>&1; then + gsort -V + return + fi + + # Fallback: awk-based numeric version sort with validation + awk -F. '{ + # Validate CalVer format: vYYYY.MM.DD or YYYY.MM.DD + if ($0 !~ /^v?[0-9]+\.[0-9]+\.[0-9]+$/) { + printf "Warning: Skipping malformed tag: %s\n", $0 > "/dev/stderr" + next + } + + # Check we have exactly 3 fields after splitting on dots + if (NF != 3) { + printf "Warning: Skipping invalid tag (wrong field count): %s\n", $0 > "/dev/stderr" + next + } + + # Save original input before modification + original = $0 + # Remove leading v and split into year, month, day + gsub(/^v/, "", $0) + + # Verify each field is numeric after field recalculation + if ($1 !~ /^[0-9]+$/ || $2 !~ /^[0-9]+$/ || $3 !~ /^[0-9]+$/) { + printf "Warning: Skipping tag with non-numeric components: %s\n", original > "/dev/stderr" + next + } + + printf "%04d.%02d.%02d %s\n", $1, $2, $3, original + }' | sort -n | cut -d' ' -f2 +} + +# Find all release tags matching vYYYY.MM.DD pattern +all_tags=$(git tag -l 'v[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9][0-9]' | version_sort_tags) + +if [ -z "$all_tags" ]; then + msg_warn "No release tags found" + exit 0 +fi + +# Get most recent tag +latest_tag=$(echo "$all_tags" | tail -n 1) + +# Extract version components +version_no_v="${latest_tag#v}" +year=$(echo "$version_no_v" | cut -d'.' -f1) +month=$(echo "$version_no_v" | cut -d'.' -f2) +day=$(echo "$version_no_v" | cut -d'.' -f3) + +major="v$year" +minor="v$year.$month" +patch="v$year.$month.$day" + +printf '\n' +msg_info "Most recent release:" +printf ' Patch: %s\n' "$patch" +printf ' Minor: %s\n' "$minor" +printf ' Major: %s\n' "$major" +printf '\n' + +# Show which tags exist +msg_info "Tags that will be deleted:" +for tag in "$patch" "$minor" "$major"; do + if check_tag_exists "$tag"; then + tag_sha=$(git rev-list -n 1 "$tag") + tag_sha_short=$(echo "$tag_sha" | cut -c1-7) + printf ' %s (points to %s)\n' "$tag" "$tag_sha_short" + fi +done +printf '\n' + +# Check if HEAD commit is a release commit +head_message=$(git log -1 --pretty=%s) +if echo "$head_message" | grep -q "^chore: update action references for release"; then + msg_warn "Last commit appears to be a release preparation commit:" + printf ' %s\n' "$head_message" + printf '\n' + reset_head=true +else + reset_head=false +fi + +# Confirm deletion +msg_warn "This will:" +printf ' 1. Delete tags: %s, %s, %s\n' "$patch" "$minor" "$major" +if [ "$reset_head" = "true" ]; then + printf ' 2. Reset HEAD to previous commit (undo release prep)\n' +fi +printf '\n' + +if ! prompt_confirmation "Proceed with rollback?"; then + msg_warn "Rollback cancelled" + exit 0 +fi +printf '\n' + +# Delete tags +msg_info "Deleting tags..." +for tag in "$patch" "$minor" "$major"; do + if check_tag_exists "$tag"; then + git tag -d "$tag" + msg_item "Deleted tag: $tag" + else + msg_notice "Tag not found: $tag (skipping)" + fi +done + +# Reset HEAD if needed +if [ "$reset_head" = "true" ]; then + printf '\n' + msg_info "Resetting HEAD to previous commit..." + git reset --hard HEAD~1 + msg_item "Reset complete" + new_head=$(git rev-parse HEAD) + new_head_short=$(echo "$new_head" | cut -c1-7) + printf 'New HEAD: %s%s%s\n' "$GREEN" "$new_head_short" "$NC" +fi + +printf '\n' +msg_done "Rollback complete" +printf '\n' +msg_warn "Note:" +printf ' Tags were deleted locally only\n' +printf ' If you had pushed the tags, delete them from remote:\n' +printf ' git push origin --delete %s %s %s\n' "$patch" "$minor" "$major" diff --git a/_tools/release.sh b/_tools/release.sh index b282352..1a0064e 100755 --- a/_tools/release.sh +++ b/_tools/release.sh @@ -2,7 +2,59 @@ # Release script for creating versioned tags and updating action references set -eu -VERSION="${1:-}" +# Parse arguments +VERSION="" +DRY_RUN=false +SKIP_CONFIRM=false +PREP_ONLY=false +TAG_ONLY=false + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) + DRY_RUN=true + shift + ;; + --yes|--no-confirm) + SKIP_CONFIRM=true + shift + ;; + --prep-only) + PREP_ONLY=true + shift + ;; + --tag-only) + TAG_ONLY=true + shift + ;; + --help|-h) + printf 'Usage: %s [OPTIONS] VERSION\n' "$0" + printf '\n' + printf 'Options:\n' + printf ' --dry-run Show what would happen without making changes\n' + printf ' --yes Skip confirmation prompt\n' + printf ' --no-confirm Alias for --yes\n' + printf ' --prep-only Only update refs and commit (no tags)\n' + printf ' --tag-only Only create tags (assumes prep done)\n' + printf ' --help, -h Show this help message\n' + printf '\n' + printf 'Examples:\n' + printf ' %s v2025.11.01\n' "$0" + printf ' %s --dry-run v2025.11.01\n' "$0" + printf ' %s --yes v2025.11.01\n' "$0" + exit 0 + ;; + -*) + printf 'Unknown option: %s\n' "$1" >&2 + printf 'Use --help for usage information\n' >&2 + exit 1 + ;; + *) + VERSION="$1" + shift + ;; + esac +done # Source shared utilities # shellcheck source=_tools/shared.sh @@ -11,15 +63,17 @@ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) . "$SCRIPT_DIR/shared.sh" if [ -z "$VERSION" ]; then - printf '%b' "${RED}Error: VERSION argument required${NC}\n" - printf 'Usage: %s v2025.10.18\n' "$0" + msg_error "VERSION argument required" + printf 'Usage: %s [OPTIONS] VERSION\n' "$0" + printf 'Use --help for more information\n' exit 1 fi # Validate version format if ! validate_version "$VERSION"; then - printf '%b' "${RED}Error: Invalid version format: $VERSION${NC}\n" - printf 'Expected: vYYYY.MM.DD (e.g., v2025.10.18)\n' + msg_error "Invalid version format: $VERSION" + printf 'Expected: vYYYY.MM.DD with zero-padded month/day (e.g., v2025.10.18, v2025.01.05)\n' + printf 'Invalid: v2025.1.5 (must be zero-padded)\n' exit 1 fi @@ -35,68 +89,201 @@ major="v$year" minor="v$year.$month" patch="v$year.$month.$day" -printf '%b' "${BLUE}Creating release $VERSION${NC}\n" +# Show dry-run banner if applicable +if [ "$DRY_RUN" = "true" ]; then + msg_plain "$YELLOW" "=== DRY RUN MODE ===" + printf 'No changes will be made to git repository\n' + printf '\n' +fi + +msg_info "Creating release $VERSION" printf ' Major: %s\n' "$major" printf ' Minor: %s\n' "$minor" printf ' Patch: %s\n' "$patch" printf '\n' +# Check if git is available (required for all modes) +if ! require_git 2>/dev/null; then + msg_error "git not available" + exit 1 +fi + +# Pre-flight checks (skip for --tag-only since prep should be done) +if [ "$TAG_ONLY" = "false" ]; then + msg_info "Running pre-flight checks..." + msg_item "git is available" + + # Check if on main branch + if ! check_on_branch "main"; then + current_branch=$(git rev-parse --abbrev-ref HEAD) + msg_error "Not on main branch (currently on: $current_branch)" + if [ "$DRY_RUN" = "false" ]; then + exit 1 + fi + else + msg_item "On main branch" + fi + + # Check if working directory is clean + if ! check_git_clean; then + msg_error "Working directory has uncommitted changes" + if [ "$DRY_RUN" = "false" ]; then + printf 'Please commit or stash changes before creating a release\n' + exit 1 + fi + else + msg_item "Working directory is clean" + fi + + # Check if patch tag already exists + if check_tag_exists "$patch"; then + msg_error "Tag $patch already exists" + if [ "$DRY_RUN" = "false" ]; then + printf 'Use a different version or delete the existing tag first\n' + exit 1 + fi + else + msg_item "Tag $patch does not exist" + fi + + printf '\n' +fi + # Get current commit SHA current_sha=$(git rev-parse HEAD) -printf '%b' "Current HEAD: ${GREEN}$current_sha${NC}\n" +printf 'Current HEAD: %s%s%s\n' "$GREEN" "$current_sha" "$NC" printf '\n' -# Update all action references to current SHA -printf '%b' "${BLUE}Updating action references to $current_sha...${NC}\n" -"$SCRIPT_DIR/update-action-refs.sh" "$current_sha" "direct" +# Confirmation prompt (skip if --yes or --dry-run) +if [ "$DRY_RUN" = "false" ] && [ "$SKIP_CONFIRM" = "false" ]; then + if ! prompt_confirmation "Proceed with release $VERSION?"; then + msg_warn "Release cancelled by user" + exit 0 + fi + printf '\n' +fi -# Commit the changes -if ! git diff --quiet; then - git add -- */action.yml - git commit -m "chore: update action references for release $VERSION +# Skip prep if --tag-only +if [ "$TAG_ONLY" = "true" ]; then + msg_info "Skipping preparation (--tag-only mode)" + printf '\n' +else + # Update all action references to current SHA + msg_info "Updating action references to $current_sha..." + if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would run: update-action-refs.sh $current_sha direct" + else + "$SCRIPT_DIR/update-action-refs.sh" "$current_sha" "direct" + fi +fi + +# Commit the changes (skip if --tag-only) +if [ "$TAG_ONLY" = "false" ]; then + if ! git diff --quiet; then + if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would add: */action.yml" + msg_warn "[DRY RUN] Would commit: update action references for release $VERSION" + else + git add -- */action.yml + git commit -m "chore: update action references for release $VERSION This commit updates all internal action references to point to the current commit SHA in preparation for release $VERSION." - # Update SHA since we just created a new commit - current_sha=$(git rev-parse HEAD) - printf '%b' "${GREEN}āœ… Committed updated action references${NC}\n" - printf '%b' "New HEAD: ${GREEN}$current_sha${NC}\n" -else - printf '%b' "${BLUE}No changes to commit${NC}\n" + # Update SHA since we just created a new commit + current_sha=$(git rev-parse HEAD) + msg_done "Committed updated action references" + printf 'New HEAD: %s%s%s\n' "$GREEN" "$current_sha" "$NC" + fi + else + msg_info "No changes to commit" + fi +fi + +# Exit early if --prep-only +if [ "$PREP_ONLY" = "true" ]; then + printf '\n' + msg_done "Preparation complete (--prep-only mode)" + msg_warn "Run with --tag-only to create tags" + exit 0 fi # Create/update tags -printf '%b' "${BLUE}Creating tags...${NC}\n" +printf '\n' +msg_info "Creating tags..." # Create patch tag -git tag -a "$patch" -m "Release $patch" -printf '%b' " ${GREEN}āœ“${NC} Created tag: $patch\n" +if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would create tag: $patch" +else + git tag -a "$patch" -m "Release $patch" + msg_item "Created tag: $patch" +fi # Move/create minor tag if git rev-parse "$minor" >/dev/null 2>&1; then - git tag -f -a "$minor" -m "Latest $minor release: $patch" - printf '%b' " ${GREEN}āœ“${NC} Updated tag: $minor (force)\n" + if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would force-update tag: $minor" + else + git tag -f -a "$minor" -m "Latest $minor release: $patch" + msg_item "Updated tag: $minor (force)" + fi else - git tag -a "$minor" -m "Latest $minor release: $patch" - printf '%b' " ${GREEN}āœ“${NC} Created tag: $minor\n" + if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would create tag: $minor" + else + git tag -a "$minor" -m "Latest $minor release: $patch" + msg_item "Created tag: $minor" + fi fi # Move/create major tag if git rev-parse "$major" >/dev/null 2>&1; then - git tag -f -a "$major" -m "Latest $major release: $patch" - printf '%b' " ${GREEN}āœ“${NC} Updated tag: $major (force)\n" + if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would force-update tag: $major" + else + git tag -f -a "$major" -m "Latest $major release: $patch" + msg_item "Updated tag: $major (force)" + fi else - git tag -a "$major" -m "Latest $major release: $patch" - printf '%b' " ${GREEN}āœ“${NC} Created tag: $major\n" + if [ "$DRY_RUN" = "true" ]; then + msg_warn "[DRY RUN] Would create tag: $major" + else + git tag -a "$major" -m "Latest $major release: $patch" + msg_item "Created tag: $major" + fi fi printf '\n' -printf '%b' "${GREEN}āœ… Release $VERSION created successfully${NC}\n" +if [ "$DRY_RUN" = "true" ]; then + msg_done "Dry run complete - no changes made" + printf '\n' + msg_info "Would have created release $VERSION" +else + msg_done "Release $VERSION created successfully" +fi printf '\n' -printf '%b' "${YELLOW}All tags point to: $current_sha${NC}\n" +msg_plain "$YELLOW" "All tags point to: $current_sha" printf '\n' -printf '%b' "${BLUE}Tags created:${NC}\n" +msg_info "Tags created:" printf ' %s\n' "$patch" printf ' %s\n' "$minor" printf ' %s\n' "$major" +printf '\n' + +# Enhanced next steps +if [ "$DRY_RUN" = "false" ]; then + msg_warn "Next steps:" + printf ' 1. Review changes: git show HEAD\n' + printf ' 2. Verify CI status: gh run list --limit 5\n' + printf ' 3. Push tags: git push origin main --tags --force-with-lease\n' + printf ' 4. Update workflow refs: make update-version-refs MAJOR=%s\n' "$major" + printf ' 5. Update README examples if needed\n' + printf ' 6. Create GitHub release: gh release create %s --generate-notes\n' "$VERSION" + printf '\n' + msg_info "If something went wrong:" + printf ' Rollback: make release-undo\n' +else + msg_warn "To execute this release:" + printf ' Run without --dry-run flag\n' +fi diff --git a/_tools/shared.sh b/_tools/shared.sh index f577f67..50cd2cd 100755 --- a/_tools/shared.sh +++ b/_tools/shared.sh @@ -14,12 +14,12 @@ YELLOW='\033[1;33m' # shellcheck disable=SC2034 NC='\033[0m' # No Color -# Validate CalVer version format: vYYYY.MM.DD +# Validate CalVer version format: vYYYY.MM.DD (zero-padded) validate_version() { version="$1" - # Check format: vYYYY.MM.DD using grep - if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}$'; then + # Check format: vYYYY.MM.DD (require zero-padding) using grep + if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{2}\.[0-9]{2}$'; then return 1 fi @@ -34,12 +34,12 @@ validate_version() { return 1 fi - # Validate month (1-12) + # Validate month (01-12) if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then return 1 fi - # Validate day (1-31) + # Validate day (01-31) if [ "$day" -lt 1 ] || [ "$day" -gt 31 ]; then return 1 fi @@ -67,12 +67,12 @@ validate_major_version() { return 0 } -# Validate minor version format: vYYYY.MM +# Validate minor version format: vYYYY.MM (zero-padded) validate_minor_version() { version="$1" - # Check format: vYYYY.MM using grep - if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{1,2}$'; then + # Check format: vYYYY.MM (require zero-padding) using grep + if ! echo "$version" | grep -qE '^v[0-9]{4}\.[0-9]{2}$'; then return 1 fi @@ -86,7 +86,7 @@ validate_minor_version() { return 1 fi - # Validate month (1-12) + # Validate month (01-12) if [ "$month" -lt 1 ] || [ "$month" -gt 12 ]; then return 1 fi @@ -94,6 +94,134 @@ validate_minor_version() { return 0 } +# Check if working directory is clean (no uncommitted changes) +check_git_clean() { + if ! has_git; then + return 1 + fi + if ! git diff --quiet || ! git diff --cached --quiet; then + return 1 + fi + return 0 +} + +# Check if currently on specified branch (default: main) +check_on_branch() { + target_branch="${1:-main}" + + if ! has_git; then + return 1 + fi + + current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1 + + if [ "$current_branch" != "$target_branch" ]; then + return 1 + fi + return 0 +} + +# Check if a git tag exists +check_tag_exists() { + tag="$1" + + if ! has_git; then + return 1 + fi + + if git rev-parse "$tag" >/dev/null 2>&1; then + return 0 + fi + return 1 +} + +# Prompt user for yes/no confirmation +# Usage: if prompt_confirmation "Continue?"; then ...; fi +prompt_confirmation() { + prompt_text="${1:-Continue?}" + timeout_seconds="${2:-30}" + + # Check if stdin is a TTY (interactive terminal) + if [ ! -t 0 ]; then + msg_error "Non-interactive session detected - cannot prompt for confirmation" + return 1 + fi + + # Check if timeout command is available for optional timeout support + if command -v timeout >/dev/null 2>&1; then + printf '%s [y/N] (timeout in %ss) ' "$prompt_text" "$timeout_seconds" + + # Use timeout command to limit read duration + # shellcheck disable=SC2016 + if response=$(timeout "$timeout_seconds" sh -c 'read -r r && echo "$r"' 2>/dev/null); then + : # read succeeded within timeout + else + printf '\n' + msg_warn "Confirmation timeout - defaulting to No" + return 1 + fi + else + # No timeout available - plain read + printf '%s [y/N] ' "$prompt_text" + read -r response || return 1 + fi + + case "$response" in + [yY]|[yY][eE][sS]) + return 0 + ;; + *) + return 1 + ;; + esac +} + +# Message output functions for consistent, colored output +# These functions provide a clean API for printing status messages + +# msg_error "message" - Print error message in red with āœ— symbol to stderr +msg_error() { + printf '%sāœ— %s%s\n' "$RED" "$1" "$NC" >&2 +} + +# msg_success "message" - Print success message in green with āœ“ symbol +msg_success() { + printf '%sāœ“ %s%s\n' "$GREEN" "$1" "$NC" +} + +# msg_done "message" - Print completion message in green with āœ… symbol +msg_done() { + printf '%sāœ… %s%s\n' "$GREEN" "$1" "$NC" +} + +# msg_info "message" - Print info/status message in blue (no symbol) +msg_info() { + printf '%s%s%s\n' "$BLUE" "$1" "$NC" +} + +# msg_warn "message" - Print warning message in yellow (no symbol) +msg_warn() { + printf '%s%s%s\n' "$YELLOW" "$1" "$NC" +} + +# msg_item "message" - Print indented item with āœ“ in green +msg_item() { + printf ' %sāœ“%s %s\n' "$GREEN" "$NC" "$1" +} + +# msg_notice "message" - Print indented notice with ℹ in blue +msg_notice() { + printf ' %sℹ%s %s\n' "$BLUE" "$NC" "$1" +} + +# msg_plain "color" "message" - Print plain colored message (no symbol) +# Usage: msg_plain "$YELLOW" "=== BANNER ===" +msg_plain() { + color="$1" + message="$2" + printf '%s%s%s\n' "$color" "$message" "$NC" +} + # Get the directory where the calling script is located get_script_dir() { cd "$(dirname -- "$1")" && pwd @@ -107,7 +235,7 @@ has_git() { # Require git to be available, exit with error if not require_git() { if ! has_git; then - printf '%b' "${RED}Error: git is not installed or not in PATH${NC}\n" >&2 + msg_error "git is not installed or not in PATH" printf 'Please install git to use this script.\n' >&2 exit 1 fi @@ -117,7 +245,7 @@ require_git() { safe_mktemp() { _temp_file="" if ! _temp_file=$(mktemp); then - printf '%b' "${RED}Error: Failed to create temp file${NC}\n" >&2 + msg_error "Failed to create temp file" exit 1 fi printf '%s' "$_temp_file"